线程是什么

程序员都熟悉编写顺序程序,也就是说,每个都有开始、执行过程和结束。在线程运行的期间任意给定时间,都有一个执行点。线程类似于顺序程序,也有开始,执行过程和结束。在线程运行期间的任何给定时间,都有一个执行点。但是,线程本身不是程序,它不能单独运行,还是需要运行在程序中,即线程是程序内的单个顺序控制流

在计算机科学中,线程的执行(**Thread of Execution**)是调度器独立管理的最小程序指令序列,调度器是操作系统的一部分。进程与线程的实现因操作系统而异,通常情况下,线程是进程的一部分。一个进程可以拥有多条线程,线程之间并发执行且共享内存资源。 :::info 线程最早称之为Tasks,直到2003年之后才逐渐流行线程这个称呼,因为CPU频率增长被内核数量增长所取代,反过来需要并发利用多个内核。 :::

线程是一个执行上下文,它是 CPU 执行指令流所需的所有信息。

假设您正在阅读一本书,并且您现在想休息一下,但您希望能够回来并从停止的确切位置继续阅读。实现这一目标的一种方法是记下页码、行号和字号。因此,您阅读一本书的执行上下文是这 3 个数字。

如果你有一个室友,而她也在使用同样的技巧,她可以在你不使用的时候拿起这本书,然后从她停下的地方继续阅读。然后你可以把它拿回来,从你原来的地方恢复它。

线程以相同的方式工作。 CPU 会给您一种错觉,即它正在同时进行多项计算。它通过在每次计算上花费一些时间来做到这一点。它可以做到这一点,因为它对每个计算都有一个执行上下文。就像您可以与朋友共享一本书一样,许多任务可以共享一个 CPU。

在技术层面上,执行上下文(线程)由 CPU 寄存器的值组成。

线程不同于进程。线程是执行的上下文,而进程是与计算相关的一堆资源。一个进程可以有一个或多个线程。 :::info 与进程关联的资源包括内存页(进程中的所有线程都具有相同的内存视图)、文件描述符(例如打开的套接字)和安全凭证(例如启动该进程的用户的 ID)。 :::

综上,线程是CPU调度的最小单位。一个线程可以看做是一个任务的执行者,在多个线程并发执行的过程中,操作系统会根据一定的算法(时间片调度算法)来决定当前应该运行那个线程。

线程状态

image.png
线程状态迁移过程

  1. 新建状态(New):新创建一个线程,还未处于可调度状态
  2. 就绪状态(Runnable):调用了线程的 start 方法,线程处于可调度状态
  3. 运行状态(Running):获得 CPU 资源,线程正在运行
  4. 阻塞等待(Blocked):线程因为某种原因放弃 CPU 使用权,暂停运行,直到再次进入就绪状态,等待 CPU 分配时间片运行
    1. 等待阻塞(WAITING):线程调用 Object.wait()/Thread.join()/LockSupport.park() 等方法进入等待状态,直到被其他线程调用 Object.notify()/Object.notifyAll()/LockSupport.unpark(Thread) 唤醒
    2. 超时等待(TIMED_WAITING):线程调用 Object.wait(long)/Thread.join(long)/Thread.sleep(long)/LockSupport.park(long) 等方法进入等待状态,直到被其他线程调用 Object.notify()/Object.notifyAll()/LockSupport.unpark(Thread) 唤醒或超时自动唤醒
    3. 同步阻塞(BLOCKED):线程请求 synchronized 锁时,该锁正在被其他的线程占有,JVM 会将该线程放入锁池,直到获取到锁
  5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期

:::warning 同步阻塞状态是线程在进入 synchronized 关键字修饰的方法或代码块(尝试获取锁) 时没有拿到锁的状态,但是阻塞在 Lock 接口 的线程状态却是等待状态, 因为 Lock 接口对于阻塞的实现均使用了 LockSupport 类中的相关方法。 :::

线程优先级

在 Java 线程中,通过整形成员变量 priority 来控制线程优先级,优先级范围是1~10,默认优先级为5,可以通过 setPriority 接口来设置线程优先级。

  1. public class Thread implements Runnable {
  2. public final static int MIN_PRIORITY = 1;
  3. /**
  4. * The default priority that is assigned to a thread.
  5. */
  6. public final static int NORM_PRIORITY = 5;
  7. public final static int MAX_PRIORITY = 10;
  8. private int priority;
  9. }

:::warning Java 中的线程优先级仅仅是给操作系统的一个建议,操作系统并不一定会采纳。而真正的调用顺序,是有操作系统的线程调度算法决定的。

Java 提供一个线程调度器来监视和控制处于 RUNNABLE 状态的线程。线程的调度策略采用抢占式,优先级高的线程比优先级低的线程会有更大的几率优先执行。在优先级相同的情况下,按照“先到先得”的原则。每个 Java 程序都有一个默认的主线程,就是通过 JVM 启动的第一个线程 main 线程。 :::

守护线程

Java 中存在两种线程:用户线程(User Thread)和守护线程(Daemon Thread)。守护线程的作用就是为其他用户线程提供服务。例如:JVM 中的垃圾回收线程(GC 线程)就是一个守护线程,负责回收不可用内存。

User 和 Daemon 两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread 已经全部退出运行了,只剩下 Daemon Thread 存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon 也就没有工作可做了,也就没有继续运行程序的必要了。

一个线程默认是非守护线程,可以通过 Thread 类的 setDaemon(boolean) 来设置。

线程的优雅关闭

线程可以理解为一段运行中的代码,或者是一个运行中的函数。既然是运行中,那么是否可以在运行过程中被强制杀死呢?答案是不能

在 Java 中,有 stop()、destroy() 等 api 能够强制杀死线程,但是官方明确表示不建议使用,因为强制杀死线程,线程使用的资源,例如文件描述符,网络连接等都不能正常关闭。因此,一个线程合理的关闭方式就是等待其运行结束,释放资源之后再退出。

设置关闭标志位

  1. class MyThread extends Thread {
  2. private boolean stop = false;
  3. @Override
  4. public void run() {
  5. while (!stop) {
  6. System.out.println("线程正在执行...");
  7. }
  8. System.out.println("退出线程...");
  9. }
  10. }

在外界通过改变 stop 的值,从而达到退出循环的效果。但是这段代码存在这样一个问题:假设里面调用了 Object.wait() 方法,进入阻塞状态,那么就没有机会调用到 while (!stop) 语句,也就不能退出线程了。这里就需要用到 InterruptedException 和 interrupt() 方法了。

InterruptedException 和 interrupt()

上面的程序,假设在主线程调用了该线程的 interrupt() 方法,是不会抛出 InterruptedException 的。只有在调用了那些声明了 InterruptedException 的方法,才会抛出异常。例如:

  1. public static void sleep(long millis) throws InterruptedException
  2. public static void sleep(long millis, int nanos) throws InterruptedException
  3. public final void join() throws InterruptedException
  4. public final void join(long millis) throws InterruptedException
  5. public final void join(long millis, int nanos) throws InterruptedException
  6. public final void wait() throws InterruptedException
  7. public final void wait(long timeout) throws InterruptedException

t.isInterrupted() 与 Thread.interrupted() 的区别

interrupt() 方法相当于是给线程发送一个唤醒信号,如果线程此时恰好处于 WAITING 或 TIMED_WAITING 状态,那么就会抛出一个 InterruptedException,并且线程被唤醒。

这两个方法都是线程用来获取自己是否接收到中断信号的。二者的区别在于:**isInterrupted()** 只读取中断状态,不修改状态;**Thread.interrupted()** 不仅读取中断状态,还会重置中断信号。

  1. if (Thread.interrupted()) {
  2. System.out.println(Thread.currentThread().isInterrupted());
  3. return;
  4. }
  5. 打印值:false
  6. if (Thread.currentThread().isInterrupted()) {
  7. System.out.println(Thread.currentThread().isInterrupted());
  8. return;
  9. }
  10. 打印值:true

线程在捕捉到 InterruptedException 之后并没有终止运行,只是会将中断状态置为true。因此,线程的退出还得开发者自己去实现。

  1. 在 catch 语句块中调用 return
  1. try {
  2. Thread.sleep(3000);
  3. } catch (InterruptedException e) {
  4. e.printStackTrace();
  5. return;
  6. }
  1. 在catch 语句块中再次调用 interrupt(),然后在线程体中判断状态并退出
    1. @Override
    2. public void run() {
    3. for (int i = 0; i < 5; i++) {
    4. if (Thread.currentThread().isInterrupted()) {
    5. return;
    6. }
    7. System.out.println("i = " + i);
    8. System.out.println(Thread.currentThread().isInterrupted());
    9. try {
    10. Thread.sleep(3000);
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. Thread.currentThread().interrupt();
    14. }
    15. }
    16. }

线程的创建方式

继承 Thread 类

  1. class MyThread extends Thread {
  2. @Override
  3. public void run() {
  4. System.out.println("线程正在执行...");
  5. }
  6. }

实现 Runnable 接口

  1. public static class MyRunnable implements Runnable {
  2. public void run() {
  3. System.out.println("线程正在执行...");
  4. }
  5. public static void main(String[] args) {
  6. MyRunnable myRunnable = new MyRunnable();
  7. new Thread(myRunnable, "MyRunnable线程").start();
  8. }
  9. }

通过 Callable 和 FutureTask 创建线程

  1. private static class MyCallable implements Callable<String> {
  2. @Override
  3. public String call() throws Exception {
  4. Thread.sleep(1000);
  5. return "MyCallable";
  6. }
  7. public static void main(String[] args) throws ExecutionException, InterruptedException {
  8. MyCallable myCallable = new MyCallable();
  9. FutureTask<String> task = new FutureTask<>(myCallable);
  10. new Thread(task).start();
  11. System.out.println("线程执行结果:" + task.get());
  12. }
  13. }

实现Runnable和Callable接口的优势

  1. 适合多个线程进行资源共享
  2. 可以避免java中单继承的限制
  3. 增加程序的健壮性,代码和数据独立
  4. 线程池只能放入Runable或Callable接口实现类,不能直接放入继承Thread的类

Runnable 和 Callable 的区别

  1. Callable 重写的是 call() 方法,Runnable 重写的方法是 run() 方法
  2. call() 方法执行后可以有返回值,run() 方法没有返回值
  3. call() 方法可以抛出异常,run() 方法不可以
  4. 运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果 。通过 Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果

线程通知与等待

Object#wait() 方法

当一个线程调用一个共享变量的 wait() 方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:

  • 其他线程调用了该共享对象的 notify() 或者 notifyAll() 方法
  • 其他线程调用了该线程的 interrupt() 方法,该线程抛出 InterruptedException 异常返回

:::warning 如果调用 wait() 方法的线程没有事先获取该对象的监视器锁,则调用 wait() 方法时调用线程会抛出 IllegalMonitorStateException 异常。也就是说,wait() 方法只能在 synchronized 关键字修饰的代码块或方法中调用。 :::

挂起的线程在没有被其他线程调用 notify()、notifyAll() 通知,或者被中断的情况下也有可能被唤醒,这就是所谓的虚假唤醒。为了防止虚假唤醒,需要取轮询该线程被唤醒的条件是否满足,不满足则继续等待:

  1. synchtonized(sLock) {
  2. while (条件不满足) {
  3. sLock.wait();
  4. }
  5. }

:::info 注意:当前线程调用共享变量的 wait() 方法后会释放当前共享变量上的锁 :::

Object#notify() 方法

一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait() 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

此外,被唤醒的线程不能马上从 wait() 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。类似 wait() 系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的 notify() 方法,否则会抛出 IllegalMonitorStateException 异常。

Object#notifyAll() 方法

不同于在共享变量上调用 notify() 函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll() 方法则会唤醒所有在该共享变量上由于调用 wait() 系列方法而被挂起的线程。

join()/yield()/sleep()

  1. 在当前线程调用另一个线程(A线程)的 join() 方法表示当前线程需要等待A线程执行结束之后在继续运行
  2. 调用 yield() 方法的线程表示自己让出 CPU 资源进入等待调度队列。实际上,yield() 方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以 yield() 方法称为“退让”,它把运行机会让给了同等优先级的其他线程。
  3. sleep() 方法表示当前线程让出 CPU 资源,并且允许低优先级的线程也能够获取到操作系统调度的机会,但是该线程所拥有的的监视器资源(比如锁)还是不会让出的

线程的上下文切换

在多线程编程中,线程个数一般都大于 CPU 个数,而每个 CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当前线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。

线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

死锁的产生必须具备以下四个条件:

  1. 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源
  2. 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源
  3. 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源
  4. 环路等待条件:指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合 {T0, T1, T2, …, Tn} 中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占用的资源,……Tn 正在等待已被 T0 占用的资源

下面是一个死锁的实例:

  1. public class DeadLock {
  2. private static Object obj1 = new Object();
  3. private static Object obj2 = new Object();
  4. public static void main(String[] args) {
  5. Thread thread1 = new Thread(() -> {
  6. synchronized (obj1) {
  7. System.out.println(Thread.currentThread() + " get obj1");
  8. try {
  9. Thread.sleep(1000);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. System.out.println(Thread.currentThread() + " will get obj2");
  14. synchronized (obj2) {
  15. System.out.println(Thread.currentThread().getName() + " get obj2");
  16. }
  17. }
  18. });
  19. Thread thread2 = new Thread(() -> {
  20. synchronized (obj2) {
  21. System.out.println(Thread.currentThread() + " get obj2");
  22. try {
  23. Thread.sleep(1000);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. System.out.println(Thread.currentThread() + " will get obj1");
  28. synchronized (obj1) {
  29. System.out.println(Thread.currentThread().getName() + " get obj1");
  30. }
  31. }
  32. });
  33. thread1.start();
  34. thread2.start();
  35. }
  36. }

如何避免死锁呢?要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但目前只有请求并持有和环路等待条件是可以被破坏的。

参考

Thread (Computing))

What Is a Thread?

What is a “thread” (really)?

对Java线程概念的理解