多线程(一)

177 阅读9分钟

1.认识线程(Thread)

1.1概念

1)线程是什么 ⼀个线程就是⼀个"执⾏流".每个线程之间都可以按照顺序执⾏⾃⼰的代码.多个线程之间"同时"执⾏ 着多份代码.

*还是回到我们之前的银⾏的例⼦中。之前我们主要描述的是个⼈业务,即⼀个⼈完全处理⾃⼰的业** *务。我们进⼀步设想如下场景:*

*⼀家公司要去银⾏办理业务,既要进⾏财务转账,⼜要进⾏福利发放,还得进⾏缴社保。*

*如果只有张三⼀个会计就会忙不过来,耗费的时间特别⻓。为了让业务更快的办理好,张三⼜找来两* *位同事李四、王五⼀起来帮助他,三个⼈分别负责⼀个事情,分别申请⼀个号码进⾏排队,⾃此就有* *了三个执⾏流共同完成任务,但本质上他们都是为了办理⼀家公司的业务。*

*此时,我们就把这种情况称为多线程,将⼀个⼤任务分解成不同⼩任务,交给不同执⾏流就分别排队* *执⾏。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(MainThread)。*

2)为啥要有线程 ⾸先,"并发编程"成为"刚需".

  • 单核CPU的发展遇到了瓶颈.要想提⾼算⼒,就需要多核CPU.⽽并发编程能更充分利⽤多核CPU资源.
  • •有些任务场景需要"等待IO",为了让等待IO的时间能够去做⼀些其他的⼯作,也需要⽤到并发编程.

其次,虽然多进程也能实现并发编程,但是线程⽐进程更轻量.

  • 创建线程⽐创建进程更快.
  • 销毁线程⽐销毁进程更快.
  • 调度线程⽐调度进程更快.

最后,线程虽然⽐进程轻量,但是⼈们还不满⾜,于是⼜有了"线程池"(ThreadPool)和"协程" (Coroutine)

3)进程和线程的区别

  • 进程是包含线程的.每个进程⾄少有⼀个线程存在,即主线程。
  • 进程和进程之间不共享内存空间.同⼀个进程的线程之间共享同⼀个内存空间.

*⽐如之前的多进程例⼦中,每个客⼾来银⾏办理各⾃的业务,但他们之间的票据肯定是不想让别⼈知道的,否则不就被其他⼈取⾛了么。⽽上⾯我们的公司业务中,张三、李四、王五虽然是不同的执⾏流,但因为办理的都是⼀家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最⼤区别。*

  • 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
  • ⼀个进程挂了⼀般不会影响到其他进程.但是⼀个线程挂了,可能把同进程内的其他线程⼀起带⾛(整个进程溃).

image.png

4)Java的线程和操作系统线程的关系

线程是操作系统中的概念.操作系统内核实现了线程这样的机制,并且对⽤⼾层提供了⼀些API供⽤⼾使⽤(例如Linux的pthread库).

Java标准库中Thread类可以视为是对操作系统提供的API进⾏了进⼀步的抽象和封装.

1.2第⼀个多线程程序

感受多线程程序和普通程序的区别:

  • 每个线程都是⼀个独⽴的执⾏流
  • 多个线程之间是"并发"执⾏的.
 import java.util.Random;
 public class ThreadDemo {
     private static class MyThread extends Thread {
         @Override
         public void run() {
             Random random = new Random();
             while (true) {
                 // 打印线程名称
                 System.out.println(Thread.currentThread().getName());
                 try {
                     // 随机停⽌运⾏ 0-9 秒
                     Thread.sleep(random.nextInt(10));
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         }
     }
     public static void main(String[] args) {
         MyThread t1 = new MyThread();
         t1.start();
         Random random = new Random();
         while (true) {
             // 打印线程名称
             System.out.println(Thread.currentThread().getName());
             try {
                 Thread.sleep(random.nextInt(10));
             } catch (InterruptedException e) {
                 // 随机停⽌运⾏ 0-9 秒
                 e.printStackTrace();
             }
         }
     }
 }

使⽤ jconsole 命令观察线程

image.png

1.3创建线程

⽅法1继承Thread类 继承Thread来创建⼀个线程类.

 class MyThread extends Thread {
     @Override
     public void run() {
         System.out.println("这⾥是线程运⾏的代码");
     }
 }

创建MyThread类的实例

  MyThread t = new MyThread();

调⽤start⽅法启动线程

 t.start(); // 线程开始运⾏

⽅法2实现 Runnable 接⼝

  1. 实现Runnable接⼝
 class MyRunnable implements Runnable {
     @Override
     public void run() {
         System.out.println("这⾥是线程运⾏的代码");
     }
 }
  1. 创建Thread类实例,调⽤Thread的构造⽅法时将Runnable对象作为target参数.
  Thread t = new Thread(new MyRunnable());
  1. 调⽤start⽅法
  t.start(); // 线程开始运⾏

对⽐上⾯两种⽅法:

  • 继承Thread类,直接使⽤this就表⽰当前线程对象的引⽤.
  • 实现Runnable接⼝,this表⽰的是 MyRunnable 的引⽤.需要使⽤Thread.currentThread()

其他变形

  • 匿名内部类创建Thread⼦类对象
 // 使⽤匿名类创建 Thread ⼦类对象
 Thread t1 = new Thread() {
     @Override
     public void run() {
         System.out.println("使⽤匿名类创建 Thread ⼦类对象");
     }
 };
  • 匿名内部类创建Runnable⼦类对象
 // 使⽤匿名类创建 Runnable ⼦类对象
 Thread t2 = new Thread(new Runnable() {
     @Override
     public void run() {
         System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
     }
 });
  • lambda表达式创建Runnable⼦类对象
 // 使⽤ lambda 表达式创建 Runnable ⼦类对象
 Thread t3 = new Thread(() -> System.out.println("使⽤匿名类创建 Thread ⼦类对象"));
 Thread t4 = new Thread(() -> {
     System.out.println("使⽤匿名类创建 Thread ⼦类对象");
 });  

1.4多线程的优势-增加运⾏速度

可以观察多线程在⼀些场合下是可以提⾼程序的整体运⾏效率的。

  • 使⽤ System.nanoTime() 可以记录当前系统的纳秒级时间戳.
  • serial 串⾏的完成⼀系列运算. concurrency 使⽤两个线程并⾏的完成同样的运算.
 public class ThreadAdvantage {
     // 多线程并不⼀定就能提⾼速度,可以观察,count 不同,实际的运⾏效果也是不同的
     private static final long count = 10_0000_0000;
     public static void main(String[] args) throws InterruptedException {
         // 使⽤并发⽅式
         concurrency();
         // 使⽤串⾏⽅式
         serial();
     }
     private static void concurrency() throws InterruptedException {
         long begin = System.nanoTime();
         // 利⽤⼀个线程计算 a 的值
         Thread thread = new Thread(new Runnable() {
             @Override  
             public void run() {
                 int a = 0;
                 for (long i = 0; i < count; i++) {
                     a--;
                 }
             }
         });
         thread.start();
         // 主线程内计算 b 的值
         int b = 0;
         for (long i = 0; i < count; i++) {
             b--;
         }
         // 等待 thread 线程运⾏结束
         thread.join();
         // 统计耗时
         long end = System.nanoTime();
         double ms = (end - begin) * 1.0 / 1000 / 1000;
         System.out.printf("并发: %f 毫秒%n", ms);
     }
     private static void serial() {
         // 全部在主线程内计算 a、b 的值
         long begin = System.nanoTime();
         int a = 0;
         for (long i = 0; i < count; i++) {
             a--;
         }
         int b = 0;
         for (long i = 0; i < count; i++) {
             b--;
         }
         long end = System.nanoTime();
         double ms = (end - begin) * 1.0 / 1000 / 1000;
         System.out.printf("串⾏: %f 毫秒%n", ms);
     }
 }

并发:399.651856毫秒
串⾏:720.616911毫秒

2.Thread类及常⻅⽅法

Thread类是JVM⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的Thread对象与之关联。

⽤我们上⾯的例⼦来看,每个执⾏流,也需要有⼀个对象来描述,类似下图所⽰,⽽Thread类的对象就是⽤来描述⼀个线程执⾏流的,JVM会将这些Thread对象组织起来,⽤于线程调度,线程管理。

image.png

2.1Thread的常⻅构造⽅法

image.png

 Thread t1 = new Thread();
 Thread t2 = new Thread(new MyRunnable());
 Thread t3 = new Thread("这是我的名字");
 Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

2.2Thread的⼏个常⻅属性

image.png

  • ID是线程的唯⼀标识,不同线程不会重复
  • 名称是各种调试⼯具⽤到
  • 状态表⽰线程当前所处的⼀个情况,下⾯我们会进⼀步说明
  • 优先级⾼的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。
  • 是否存活,即简单的理解,为run⽅法是否运⾏结束了
 • 线程的中断问题,下⾯我们进⼀步说明
     public class ThreadDemo {
         public static void main(String[] args) {
             Thread thread = new Thread(() -> {
                 for (int i = 0; i < 10; i++) {
                     try {
                         System.out.println(Thread.currentThread().getName() + ": 我还
                                            Thread.sleep(1 * 1000);
                                            } catch (InterruptedException e) {
                                                e.printStackTrace();
                                            }
                                            }                                           System.out.println(Thread.currentThread().getName() + ": 我即将死去")
                                            });
                         System.out.println(Thread.currentThread().getName()
                                            + ": ID: " + thread.getId());
                         System.out.println(Thread.currentThread().getName()
                                            + ": 名称: " + thread.getName());
                         System.out.println(Thread.currentThread().getName()
                                            + ": 状态: " + thread.getState());
                         System.out.println(Thread.currentThread().getName()
                                            + ": 优先级: " + thread.getPriority());
                         System.out.println(Thread.currentThread().getName()
                                            + ": 后台线程: " + thread.isDaemon());
                         System.out.println(Thread.currentThread().getName()
                                            + ": 活着: " + thread.isAlive());
                         System.out.println(Thread.currentThread().getName()
                                            + ": 被中断: " + thread.isInterrupted());
                         thread.start();
                         while (thread.isAlive()) {}
                         System.out.println(Thread.currentThread().getName()
                                            + ": 状态: " + thread.getState());
                     }
                 }

2.3启动⼀个线程-start()

之前我们已经看到了如何通过覆写run⽅法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运⾏了。

  • 覆写run⽅法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把李四、王五叫过来了
  • ⽽调⽤start()⽅法,就是喊⼀声:”⾏动起来!“,线程才真正独⽴去执⾏了。

image.png

调⽤start⽅法,才真的在操作系统的底层创建出⼀个线程.

2.4中断⼀个线程

李四⼀旦进到⼯作状态,他就会按照⾏动指南上的步骤去进⾏⼯作,不完成是不会结束的。但有时我们需要增加⼀些机制,例如⽼板突然来电话了,说转账的对⽅是个骗⼦,需要赶紧停⽌转账,那张三该如何通知李四停⽌呢?这就涉及到我们的停⽌线程的⽅式了。

⽬前常⻅的有以下两种⽅式:

  1. 通过共享的标记来进⾏沟通
  2. 调⽤interrupt()⽅法来通知

⽰例-1:使⽤⾃定义的变量来作为标志位.

 public class ThreadDemo {
     private static class MyRunnable implements Runnable {
         public volatile boolean isQuit = false;
         @Override
         public void run() {
             while (!isQuit) {
                 System.out.println(Thread.currentThread().getName()
                                    + ": 别管我,我忙着转账呢!");
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
             System.out.println(Thread.currentThread().getName()
                                + ": 啊!险些误了⼤事");
         }
     }
     public static void main(String[] args) throws InterruptedException {
         MyRunnable target = new MyRunnable();
         Thread thread = new Thread(target, "李四");
         System.out.println(Thread.currentThread().getName()
                            + ": 让李四开始转账。");
         thread.start();
         Thread.sleep(10 * 1000);
         System.out.println(Thread.currentThread().getName()
                            + ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");
         target.isQuit = true;
     }
 }

⽰例-2: 使⽤ Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替⾃定义标志位.

Thread内部包含了⼀个boolean类型的变量作为线程是否被中断的标记.

image.png

  • 使⽤thread对象的 interrupted() ⽅法通知线程结束
 public class ThreadDemo {
     private static class MyRunnable implements Runnable {
         @Override
         public void run() {
             // 两种⽅法均可以
             while (!Thread.interrupted()) {
                 //while (!Thread.currentThread().isInterrupted()) {
                 System.out.println(Thread.currentThread().getName()
                                    + ": 别管我,我忙着转账呢!");
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                     System.out.println(Thread.currentThread().getName()
                                        + ": 有内⻤,终⽌交易!");
                     // 注意此处的 break
                     break;
                 }
             }
             System.out.println(Thread.currentThread().getName()
                                + ": 啊!险些误了⼤事");
         }
     }
     public static void main(String[] args) throws InterruptedException {
         MyRunnable target = new MyRunnable();
         Thread thread = new Thread(target, "李四");
         System.out.println(Thread.currentThread().getName()
                            + ": 让李四开始转账。");
         thread.start();
         Thread.sleep(10 * 1000);
         System.out.println(Thread.currentThread().getName()
                            + ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");
         thread.interrupt();
     }
 }

thread收到通知的⽅式有两种:

  1. 如果线程因为调⽤wait/join/sleep等⽅法⽽阻塞挂起,则以InterruptedException异常的形式通知,清除中断标志
    ◦ 当出现InterruptedException的时候,要不要结束线程取决于catch中代码的写法.可以选择忽略这个异常,也可以跳出循环结束线程.
  1. 否则,只是内部的⼀个中断标志被设置,thread可以通过
    ◦ Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志这种⽅式通知收到的更及时,即使线程正在sleep也可以⻢上收到。

2.5等待⼀个线程-join()

有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要⼀个⽅法明确等待线程的结束 。

 public class ThreadDemo {
     public static void main(String[] args) throws InterruptedException {
         Runnable target = () -> {
             for (int i = 0; i < 10; i++) {
                 try {
                     System.out.println(Thread.currentThread().getName()
                                        + ": 我还在⼯作!");
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
             System.out.println(Thread.currentThread().getName() + ": 我结束了!")
         };
 ​
         Thread thread1 = new Thread(target, "李四");
         Thread thread2 = new Thread(target, "王五");
         System.out.println("先让李四开始⼯作");
         thread1.start();
         thread1.join();
         System.out.println("李四⼯作结束了,让王五开始⼯作");
         thread2.start();
         thread2.join();
         System.out.println("王五⼯作结束了");
     }
 }

image.png

2.6获取当前线程引⽤

这个⽅法我们已经⾮常熟悉了

 public class ThreadDemo {
     public static void main(String[] args) {
         Thread thread = Thread.currentThread();
         System.out.println(thread.getName());
     }
 }

image.png

2.7休眠当前线程

也是我们⽐较熟悉⼀组⽅法,有⼀点要记得,因为线程的调度是不可控的,所以,这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的。

image.png

 public class ThreadDemo {
     public static void main(String[] args) throws InterruptedException {
         System.out.println(System.currentTimeMillis());
         Thread.sleep(3 * 1000);
         System.out.println(System.currentTimeMillis());
     }
 }