服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - 面霸的自我修养:Java线程专题

面霸的自我修养:Java线程专题

2023-07-31 03:32未知服务器之家 Java教程

大家好,我是王有志。今天和大家分享 33 道 Java 线程相关的八股文,主要涉及并发编程和线程中的基础概念,Object类,Thread 类中相关重点方法以及 Runnable 接口和 Callable 接口的内容。 王有志,一个分享硬核Java技术的互金摸鱼侠

面霸的自我修养:Java线程专题 大家好,我是王有志。今天和大家分享 33 道 Java 线程相关的八股文,主要涉及并发编程和线程中的基础概念,Object类,Thread 类中相关重点方法以及 Runnable 接口和 Callable 接口的内容。

王有志,一个分享硬核Java技术的互金摸鱼侠
加入Java人的提桶跑路群:共同富裕的Java人

平时我在网上冲浪的时候,收集了不少八股文和面试文,内容虽然多,但质量上良莠不齐,主打一个不假思索的互相抄,使得很多错误内容一代代得“传承”了下来。所以,我对收集的内容做了归纳和整理,通过查阅资料重新做了解答,并给出了每道八股文评分。

好了,废话不多说我们进入正题,今天的主题是 Java 面试中线程相关的八股文,主要涉及以下内容:

  • 并发编程的基础概念
  • 线程的基础概念
  • Java 中的 Thread ,Runnable,Callable

由于本人水平有限,解答过程中难免出现错误,还请大家以批评指正为主,尽量不要喷~~

Tips

  • 八股文通常出现在面试第一二轮,是面试的敲门砖,第三轮的重点是如何讲好做过的项目;
  • 八股文的主要来源是各机构(Java 之父和继父,某灵,某泡,某客等)及各个博主的文档;
  • 小部分八股文来自我个人及朋友真实面试经历,题目上会有“真”的标注
  • 本文已完成 PDF 文档的制作,提取关键字【面霸的自我修养】。

概念篇

这部分是并发编程中的基础概念和理论基础,整体难度较低,并且当你有了一定的工作年限后,很少会涉及这类问题,大家以了解为主。

并发与并行

并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

并行,在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。对比地,并发是指:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)

并发在宏观上是同时执行,但微观上是交替执行,而并行无论是宏观还是微观,都是同时执行。

Tips:打个比方,并行像是打开了两盏灯,它们同时处于亮起的状态;而并发就是一盏灯,肉眼看起来是“常亮”状态,但实际在交流电的作用下,灯一直再闪烁,只是肉眼无法观察到。

参考资料:并发(百度百科),并行(百度百科)


同步与异步

同步:同步,可以理解为在通信时、函数调用时、协议栈的相邻层协议交互时等场景下,发信方与收信方、主调与被调等双方的状态是否能及时保持状态一致。如果一方完成一个动作后,另一方立即就修改了自己的状态,就是同步。

异步:是指调用方发出请求就立即返回,请求甚至可能还没到达接收方,比如说放到了某个缓冲区中,等待对方取走或者第三方转交;而调用结果是通过接收方主动推送,或调用方轮询来得到。

参考资料:同步(维基百科)


阻塞与非阻塞

阻塞与非阻塞指的是程序在等待调用结果时的状态。

阻塞(Blocking):被调用时,线程会被挂起/暂停/阻塞,直到该操作完成,返回结果后再执行后续操作。此时,程序无法进行其它操作,会一直等到调用结果返回。

非阻塞(Non-blocking):被调用时,即便操作尚未完成和拿到结果,线程也不会被挂起/暂停/阻塞,程序可以继续执行后序操作。


线程与进程

进程(process),曾经是分时系统的基本运作单位。在面向进程设计的系统中,是程序的基本执行实体;在面向线程设计的系统中,进程本身不是基本执行单位,而是线程的容器

线程(thread),在计算机科学中,是将进程划分为两个或多个线程(实例)或子进程,由单处理器(单线程)或多处理器(多线程)或多核处理系统并发执行。

进程与线程之间的差别:

进程 线程
进程拥有自己的内存空间,文件句柄,系统信号和环境变量等 所有线程共享进程的资源,包括内存空间,文件句柄,系统信号等
进程是独立的执行单元,拥有自己的堆栈空间,需要使用进程间通信机制进行数据交换 线程是进程内部的执行单元,共享进程的地址空间,可以直接访问进程的全局变量和堆空间
进程间切换开销较大。进程间的切换比线程间的切换耗时和开销都大得多,因为进程切换需要保存和恢复更多的状态信息,如内存映像、文件句柄、系统信号等 线程间切换开销较小。线程的切换只需要保存和恢复少量的寄存器和堆栈信息
进程间资源隔离明显,进程间安全性较高 线程间共享资源,容易引起竞态条件,线程间安全性较低

参考资料:进程(维基百科),线程(维基百科)


并发编程的3要素

原子性:指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行;
可见性:软件工程中,是指对象间的可见性,含义是一个对象能够看到或者能够引用另一个对象的能力;
有序性:有序性是指对于多个线程或进程执行的操作,其执行顺序与程序代码中的顺序保持一致或符合预期的规则。

Tips:未在维基百科和百度百科中查找到有序性的解释,这里采用了 ChatGPT 的解释。

参考资料:关于线程你必须知道的8个问题(上),原子性(百度百科),可见性(百度百科)


线程饥饿

线程饥饿(Thread Starvation),指的是在多线程的竞争环境中,某个线程长时间无法获取所需资源,或长时间无法得到调度,导致任务无法完成的状态。

常见产生的线程饥饿的原因如下:

  • 资源竞争,多个线程竞争必须资源,某个线程长时间无法获取到资源;
  • 线程优先级,线程优先级设置不当,导致优先级较低的线程长时间无法得到调度;
  • 锁竞争,同资源竞争,只不过此时竞争的是保护资源的锁。

上下文切换

多个线程共用同一个 CPU 时,CPU 时间从一个线程切换到另一个线程的过程。在这个过程中,需要保存线程的上下文信息(如:程序计数器,寄存器状态,堆栈指针等),同时加载另一个线程的上线文信息,使得系统能够正确执行。

Tips

参考资料:上下文切换(维基百科)


真:死锁及解决死锁

面试公司:苏宁,质数金融,网易

死锁(deadlock),当两个以上的运算单元,双方都在等待对方停止执行,以获取系统资源,但是没有一方提前退出时,就称为死锁。

形成死锁需要 4 个条件:

  • 互斥条件(Mutual Exclusion):资源只能被一个进程/线程占用。当一个进程/线程获取了资源,其他进程/线程无法访问该资源,只能等待资源被释放;
  • 请求与保持条件(Hold and Wait):进程/线程在持有资源的同时,继续请求其他资源,并不释放持有的资源;
  • 不剥夺条件(No Preemption):已经被持有的资源不能被强制剥夺,只有进程/线程主动释放后才能被其他进程/线程获取;
  • 循环等待条件(Circular Wait):存在一组进程/线程,互相请求彼此所持有的资源,线程了循环等待的环路。

面霸的自我修养:Java线程专题

解决死锁问题的核心是打破4项条件其中的一项即可:

  • 破坏互斥条件:允许资源被同时访问;
  • 破坏请求与保持条件:申请其它资源前,进行/线程需要释放当前资源,避免阻塞其它线程;
  • 破坏不可剥夺条件:允许优先级较高的进程/线程,强制剥夺其它线程持有的资源;
  • 破坏循环等待条件:对资源进行编号,强制获取资源时按照编号顺序进行获取。

参考资料:死锁(维基百科),关于线程你必须知道的8个问题(下)


真:线程通信

面试公司:有利网

并发编程领域常见的 2 个线程间通信模型:共享内存和消息传递

共享内存:指的是多个线程运行在不同核心上,任何核心缓存上的数据修改后,刷新到主内存后,其他核心更新自己的缓存。

面霸的自我修养:Java线程专题

消息传递:多个线程可以通过消息队列进行通信,线程可以将消息发送到队列中,其他线程可以从队列中获取消息并进行处理。

传统面向对象编程语言通常会采用共享内存的方式进行线程间的通信,如 Java,C++等。但 Java 可以通过 Akka 实现 Actor 模型的消息传递。Golang 则是消息传递的忠实拥趸,《Go Proverbs》中第一句便是:

Don't communicate by sharing memory, share memory by communicating.

参考资料:共享内存(维基百科),消息传递(维基百科),管道(维基百科),共享内存(百度百科),消息传递(百度百科)


真:多线程优势与挑战

面试公司:苏宁

运用多线程的根本原因是“压榨”硬件性能,提高程序效率

  • 发挥多核 CPU 的优势:多核 CPU 下,单线程程序同一时间只会使用一个核心,其余核心处于空闲状态,造成了资源的浪费;
  • 提高系统资源的用率:即便是单核场景下,程序在等待响应或文件 IO 操作时 CPU 长时间空闲,此时利用多线程可以充分利用 CPU。

但引入多线程也带来了一些挑战:

  • 上下文切换:具体请参考上文;
  • 死锁:具体请参考上文;
  • 资源限制:过多的线程会消耗大量的内存,以及产生更多的 CPU 竞争,会影响程序的性能。设计时应该根据硬件和程序合理的控制线程数量;
  • 线程安全问题:如果对共享资源进行并发访问,可能会造成数据一致性问题,或其他意料之外的结果。
  • 编程难度的提升:因为线程是并发执行的,并存在不确定行为,如:线程的执行顺序导致结果的差异,这种情况会造成开发与调试的困难。

参考资料:关于线程你必须知道的8个问题(下),Java并发编程的艺术(豆瓣)


线程安全

线程安全:指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的公用变量,使程序功能正确完成。

通俗点可以理解为,程序在多线程环境中与单线程环境中的执行结果一致。

Tips:这与 JMM 中提到的终极目标 as-if-serial 语义稍有差别,as-if-serial 语义强调无论如何重排序,单线程场景下的语义不能被改变(或者说执行结果不变)。

参考资料:线程安全(维基百科),线程安全(百度百科),深入理解JMM和Happens-Before

原理篇

接下来是 Java 应用篇,主要是关于 Java 中线程,Thread 类,Runnable 接口,Callable 接口以及 Future 接口的内容。


Java 中的线程

早期的 Linux 系统并不支持线程,但可以通过编程语言模拟实现“线程”,但其本质还是进程,这时我们认为 Java 中的线程是用户线程。到了 2003 年,RedHat 初步完成了 NPTL(Native POSIX Thread Library)项目,通过轻量级进程实现了服务号 POSIX 标准的线程,这时 Java 中的线程是内核线程。因此运行在现代服务器上的 Java 程序,使用的 Java 线程都会映射的到一个内核线程上

所以我们可以得到这样一个式子:\(Java线程 \approx  操作系统内核线程 \approx 操作系统轻量级进程\)

那么对于线程的调度方式来说,我们可以得到:\(Java线程的调度方式 \approx 操作系统进程的调度方式\)

恰好,Linux中使用了抢占式进程调度方式。因此,并不是JVM中实现了抢占式线程调度方式,而是Java使用了Linux的进程调度方式,Linux选择了抢占式进程调度方式

参考资料:关于线程你必须知道的8个问题(下)


真:创建线程的方式

面试公司:苏宁

Java 中只有一种创建线程的方式。从 Java 层面来看,可以认为执行Thread thread = new Thread()就创建了线程;而调用Thread#start则是操作系统层面的线程创建于启动。

// 创建Java层面的线程
Thread thread = new Thread();
// 创建系统层面的线程
thread.start();

Tips:通常网上会给出至少 4 种创建线程的方式:

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 通过线程池创建

但这是一个错误的结论,实现Runnable接口或是实现Callable接口,其主要目的是为了重写Runnable#run方法,以实现业务逻辑,而要真正的创建并启动一个 Java 线程还是要创建 Thread 对象,并调用Thread#start方法。

Tips:还有的资料中搞出了 6 种创建线程的方式~~

参考资料:关于线程你必须知道的8个问题(上)


真:线程的状态与状态转换

面试公司:京东,百度

Java 中定义了 6 种线程状态:

  • NEW(新建):创建线程后尚未启动(未调用Thread.start方法);
  • RUNNABLE(可运行):可运行状态的线程在Java虚拟机中等待调度线程选中获取CPU时间片;
  • BLOCKED(阻塞):等待监视器锁而阻塞的线程状态,处于阻塞状态的线程正在等待监视器锁进入同步的代码块/方法,或者在调用Object.wait之后重新进入同步的代码块/方法;
  • WAITING(等待):线程处于等待状态,处于等待状态的线程正在等待另一个线程执行的特定操作(通知或中断);
  • TIMED_WAITING(超时等待):线程处于超时等待状态,与等待状态不同的是,在指定时间后,线程会被自动唤醒;
  • TERMINATED(终止):线程执行结束。

线程状态定义为 Thread 的内部类 state:

public enum State {
  NEW,
  RUNNABLE,
  BLOCKED,
  WAITING,
  TIMED_WAITING,
  TERMINATED;
}

线程状态的转换请参考下图:

面霸的自我修养:Java线程专题

参考资料:关于线程你必须知道的8个问题(上)


真:Object#wait 方法的作用

面试公司:苏宁

Object#wait 使线程等待,同时释放锁,线程进入 WAITING 或 TIMED_WAITING 状态Object#wait有 3 个重载方法:

public final void wait() throws InterruptedException;
    
public final native void wait(long timeoutMillis) throws InterruptedException;
    
public final void wait(long timeoutMillis, int nanos) throws InterruptedException;

由于Object#wait释放锁,因此需要在同步块(synchronized 块)中调用,因为只有先获得锁,才有的释放。

Tips:面试中常常用来与Thread#sleep进行比较。

参考资料:关于线程你必须知道的8个问题(中)


Object#notify/Object#notifyAll 方法的作用

Object#notifyObject#notifyAll都是用来唤醒线程的。Object#notify随机唤醒一个等待中的线程Object#notifyAll唤醒所有等待中的线程。通过Object#notify与Object#notifyAll唤醒的线程并不会立即执行,而是加入了争抢内置锁的队列,只有成功获取到锁的线程才会继续执行。

参考资料:关于线程你必须知道的8个问题(中)


为什么要在循环中调用Object#wait方法?

如果不在循环中检查等待条件,等待状态中的线程可能会被错误的唤醒,此时跳过等待条件的检查可能会造成意想不到的问题。例如:生产者与消费者的场景。

    public static void main(String[] args) {
      Product product = new Product(0);
      new Thread(() -> {
        for (int i = 0; i < 3; i++) {
          try {
            product.decrement();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
        }
        System.out.println(Thread.currentThread().getName() + ",状态:" + Thread.currentThread().getState());
      }, "consumer-1").start();
      
      new Thread(() -> {
        for (int i = 0; i < 3; i++) {
          try {
            product.decrement();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
        }
      }, "consumer-2").start();
      
      new Thread(() -> {
        for (int i = 0; i < 3; i++) {
          try {
            product.decrement();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
        }
      }, "consumer-3").start();
      
      new Thread(() -> {
        for (int i = 0; i < 9; i++) {
          try {
            product.increment();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
        }
        , "producer").start();
    }
    
    static class Product {
        private int count;
        private Product(int count) {
            this.count = count;
        }
        /**
         * 生产
         */
        private synchronized void increment() throws InterruptedException {
            if (this.count > 0) {
                this.wait();
            }
            count++;
            System.out.println("[" + Thread.currentThread().getName() + "]生产产品,当前总数:" + this.count);
            this.notifyAll();
        }
        /**
         * 消费
         */
        private synchronized void decrement() throws InterruptedException {
            if (this.count == 0) {
                this.wait();
            }
            count--;
            System.out.println("[" + Thread.currentThread().getName() + "]消费产品,当前总数:" + this.count);
            this.notifyAll();
        }
    }

修改方案非常简单,只需要在循环中检进入等待的条件即可,代码修改后如下:

    static class Product {
    
      private synchronized void increment() throws InterruptedException {
        while (this.count > 0) {
          this.wait();
        }
        count++;
        System.out.println("[" + Thread.currentThread().getName() + "]生产产品,当前总数:" + this.count);
        this.notifyAll();
      }
      
      private synchronized void decrement() throws InterruptedException {
        while (this.count == 0) {
          this.wait();
        }
        count--;
        System.out.println("[" + Thread.currentThread().getName() + "]消费产品,当前总数:" + this.count);
        this.notifyAll();
      }
    }

参考资料:关于线程你必须知道的8个问题(中)


为什么 Object#wait,Object#notify 和 Object#notifyAll 方法不放在 Thread 类中?

Java 提供的内置锁(ObjectMonitor)是对象级别的,即每个对象都有一个内置锁。而Obejct#waitObejct#notifyObejct#notifyAll涉及到内置锁的操作,这与线程无关,只与对象有关,因此将它们放在所有对象的父类 Object 中。

参考资料:关于线程你必须知道的8个问题(中)


为什么 Object#wait,Object#notify和Object#notifyAll这些方法要在同步块中调用?

因为这 3 个方法都涉及到对内置锁的操作。

Object#wait方法释放锁,而Object#notifyObject#notifyAll用于通知其它线程当前锁可用,而执行这些操作的奇谭提是持有锁,或知道锁的状态,因此必须在 synchronized 中调用。

参考资料:关于线程你必须知道的8个问题(中)


Thread#start 与 Thread#run 的区别

Thread#start创建了操作系统层面的线程,并启动线程调用Thread#run。而Thread#run只是Runnable接口的实现,并不会创建并启动线程。

    public synchronized void start() {
      if (threadStatus != 0)
        throw new IllegalThreadStateException();
      group.add(this);
      boolean started = false;
      try {
        // 调用JNI方法,创建系统层面线程
        start0();
        started = true;
      } finally {
        try {
          if (!started) {
            group.threadStartFailed(this);
          }
        } catch (Throwable ignore) {
        }
      }
    }

第 8 行中调用了 JNI 方法private native void start0(),在 JVM 的实现中,该方法创建了操作系统层面的线程。

    private Runnable target;
    
    @Override
    public void run() {
      if (target != null) {
        target.run();
      }
    }

Thread#run只是对Runnable接口的实现,并调用了成员变量 target 的 run 方法。

参考资料:关于线程你必须知道的8个问题(中)


真:多次调用 Thread#start 会发生什么?

面试公司:苏宁

多次调用Thread#start方法会抛出IllegalThreadStateException异常。

Thread#start方法的源码:

    public synchronized void start() {
      if (threadStatus != 0)
        throw new IllegalThreadStateException();
      group.add(this);
      boolean started = false;
      try {
        start0();
        started = true;
      } finally {
        try {
          if (!started) {
            group.threadStartFailed(this);
          }
        } catch (Throwable ignore) {
        }
      }
    }

调用Thread#start方法,会先对threadStatus进行判断,只有当threadStatus == 0时,Thread#start才能正常执行,否则抛出IllegalThreadStateException异常。

threadStatus实际上是 Thread 内部类 state 的映射,以下涉及java.lang.Thread类和jdk.internal.misc.VM类的相关代码:

    public class Thread implements Runnable {
      public State getState() {
        return jdk.internal.misc.VM.toThreadState(threadStatus);
      }
      
      public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
      }
    }
    
    public class VM {
      private static final int JVMTI_THREAD_STATE_ALIVE = 0x0001;                    // 1   , 0000 0000 0001
      private static final int JVMTI_THREAD_STATE_TERMINATED = 0x0002;               // 2   , 0000 0000 0010
      private static final int JVMTI_THREAD_STATE_RUNNABLE = 0x0004;                 // 4   , 0000 0000 0100
      private static final int JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER = 0x0400; // 1024, 0100 0000 0000
      private static final int JVMTI_THREAD_STATE_WAITING_INDEFINITELY = 0x0010;     // 16  , 0000 0001 0000
      private static final int JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT = 0x0020;     // 32  , 0000 0010 0000
    
      public static Thread.State toThreadState(int threadStatus) {
        if ((threadStatus & JVMTI_THREAD_STATE_RUNNABLE) != 0) {
          return RUNNABLE;
        } else if ((threadStatus & JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER) != 0) {
          return BLOCKED;
        } else if ((threadStatus & JVMTI_THREAD_STATE_WAITING_INDEFINITELY) != 0) {
          return WAITING;
        } else if ((threadStatus & JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT) != 0) {
          return TIMED_WAITING;
        } else if ((threadStatus & JVMTI_THREAD_STATE_TERMINATED) != 0) {
          return TERMINATED;
        } else if ((threadStatus & JVMTI_THREAD_STATE_ALIVE) == 0) {
          return NEW;
        } else {
          return RUNNABLE;
        }
      }
    }

参考资料:关于线程你必须知道的8个问题(中)


可以直接调用Thread#run方法么?

可以直接调用Thread#run方法,和调用普通方法一样,会在调用线程中执行,例如:

    public static void main(String[] args) {
      Thread t1 = new Thread(()-> System.out.println("直接调用run方法"));
      t1.run();
    }

上述代码中,Thread#run方法由主线程执行,并不会启动新线程,因为Thread#run只是对接口方法Runnable#run的实现:

    public class Thread implements Runnable {
      private Runnable target;
      
      @Override
      public void run() {
        if (target != null) {
          target.run();
        }
      }
    }

参考资料:关于线程你必须知道的8个问题(中)


真:Thread#sleep 方法的作用

面试公司:苏宁

Thread#sleep使线程进入休眠,但不会释放锁(锁指的是 synchronized 中使用的 Java 内置错,即 ObjectMonitor),线程进入 TIMED_WATING  状态。确切的说Thread#sleep在 JVM 的实现中,并不执行锁相关操作的逻辑,所以实际中也谈不上释放不释放。

Thread#sleep有 2 个重载方法:

    public static native void sleep(long millis) throws InterruptedException;
    
    public static void sleep(long millis, int nanos) throws InterruptedException;

Object#waitThread#sleep的区别:

Object#wait Thread#sleep
作用 线程进入暂停状态(WAITING/TIMED_WAITING) 线程进入休眠状态(TIMED_WATING)
CPU 资源 释放 释放
内置锁(ObjectMonitor) 释放 不释放

重点关注在 CPU 资源和内置锁的持有与释放上的差别即可。

参考资料:关于线程你必须知道的8个问题(中)


Thread#sleep(0) 有什么作用?

调用Thread#sleep(0)真实的让出 CPU 时间,从而触发 CPU 时间片的竞争。

jvm.cpp 源码:

    JVM_ENTRY(void, JVM_Sleep(JNIEnv* env, jclass threadClass, jlong millis))
      HOTSPOT_THREAD_SLEEP_BEGIN(millis);
      EventThreadSleep event;
      if (millis == 0) {
        os::naked_yield();
      } else {
        ThreadState old_state = thread->osthread()->get_state();
        thread->osthread()->set_state(SLEEPING);
        if (os::sleep(thread, millis, true) == OS_INTRPT) {
          if (!HAS_PENDING_EXCEPTION) {
            if (event.should_commit()) {
              post_thread_sleep_event(&event, millis);
            }
            HOTSPOT_THREAD_SLEEP_END(1);
          }
        }
        thread->osthread()->set_state(old_state);
      }
      HOTSPOT_THREAD_SLEEP_END(0);
    JVM_END

第 4 行判断millis == 0成功后会执行os::naked_yield(),此时作用与Thread#yield相同。

参考资料:关于线程你必须知道的8个问题(中)


Thread#yield 方法的作用

调用Thread#yield使当前线程让出 CPU 时间,从而触发 CPU 时间片的竞争。另外,与Thread#sleep一样,Thread#yield也不会释放锁。

JVM 中的实现:

    JVM_ENTRY(void, JVM_Yield(JNIEnv *env, jclass threadClass))
      if (os::dont_yield()) {
        return;
      }
      os::naked_yield();
    JVM_END

与调用Thread#sleep(0)的实现一样。

TipsThread#yield只是让出了时间片,但又可能会立即抢夺回来,例如:所有线程必须持有锁才能执行,持有锁的线程调用Thread#yield让出 CPU 时间片,但并未释放锁,其他线程无法执行,只能持有线程的锁继续获取 CPU 时间片。

参考资料:关于线程你必须知道的8个问题(中)


Thread#join 方法的作用

Thread#join等待其他线程运行结束,线程进入 WAITING 或TIMED_WAITING 状态。假如我们右如下代码:

    Thread t1 = new Thread(()- >{
      // 业务逻辑
    });
    
    Thread t2 = new Thread(()- >{
      t1.join();
      // 业务逻辑
    });
    
    t1.start();
    t2.start();

上述代码中,主线程启动线程 t1 和 t2 后,线程 t2 会等待线程 t1 执行结束后再继续执行,即谁执行了线程 t2 执行了t1.join后等待线程 t1 执行完毕后再执行。

参考资料:关于线程你必须知道的8个问题(中)


Thread#interrupt 方法的作用

Thread#interrupt方法表示中断线程,但 JVM 并不会立即中断线程,仅仅是将线程标记为中断状态,随后尝试唤醒处于 sleep/wait/park 中的线程,真正的中断线程的执行是在操作系统获取到该线程的中断状态标记开始的。

需要注意,当线程调用了Object#waitThread#joinThread#sleep方法后再调用Thread#interrupt方法会抛出异常 InterruptedException。这点在 Java 源码的注释上也有说明:

If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.

Thread#interrupt0方法在JVM的核心源码位于 thread.cpp 中:

    void os::interrupt(Thread* thread) {
      OSThread* osthread = thread->osthread();
      if (!osthread->interrupted()) {
        osthread->set_interrupted(true);
        OrderAccess::fence();
        ParkEvent * const slp = thread->_SleepEvent ;
        if (slp != NULL)
          slp->unpark() ;
      }
      if (thread->is_Java_thread())
        ((JavaThread*)thread)->parker()->unpark();
      
      ParkEvent * ev = thread->_ParkEvent ;
      if (ev != NULL) 
        ev->unpark() ;
    }

参考资料:关于线程你必须知道的8个问题(中),thread.cpp


如何停止一个正在运行的线程?

  • 使用Thread#interrupt方法
  • 使用Thread#stop方法(该方法已被废弃)

Runnable接口和Callable接口

Runnable 和 Callable 都是线程中用来执行业务逻辑方法的接口。

Runnable 接口:

    @FunctionalInterface
    public interface Runnable {
      public abstract void run();
    }

Callable 接口:

    public interface Callable<V> {
        V call() throws Exception;
    }

Runnable 接口没有返回值,而 Callable 接口是有返回值的,可以借助 Futur 或 FutureTask 获取线程运行的结果,举个例子:

    public class ByCallable  {
      public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main的线程:" + Thread.currentThread().getName());
        Callable<String> callable = new MyCallable();
        FutureTask <String> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        System.out.println("MyCallable的执行线程:" + futureTask.get());
      }
      
      static class MyCallable implements Callable<String> {
        @Override
        public String call() {
          System.out.println("MyCallable的线程:" + Thread.currentThread().getName());
          return Thread.currentThread().getName();
        }
      }
    }

应用篇

这部分是考察线程相关内容的应用,常见的是对线程的等待与唤醒的使用。

使用3个线程交替打印100次字母abc。

方法 1:引入同步状态(AtomicInteger)

创建变量同步状态 state,通过同步状态确认何时打印对应的字母:

  • 当state % 3 == 1时打印字母A,同时更新同步状态;
  • 当state % 3 == 2时打印字母B,同时更新同步状态;
  • 当state % 3 == 0时打印字母C,同时更新同步状态。

代码实现如下:

    private static final AtomicInteger STATE = new AtomicInteger(1);
    
    private static void useAtomicSyncState() {
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          if (STATE.get() % 3 == 1) {
            System.out.print("A,");
            STATE.compareAndSet(i * 3 + 1, i * 3 + 2);
            i++;
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          if (STATE.get() % 3 == 2) {
            System.out.print("B,");
            STATE.compareAndSet(i * 3 + 2, i * 3 + 3);
            i++;
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          if (STATE.get() % 3 == 0) {
            System.out.print("C");
            System.out.println();
            STATE.compareAndSet(i * 3 + 3, i * 3 + 4);
            i++;
          }
        }
      }).start();
    }

Tips:使用AtomicInteger类型是出于以下两点考虑:

  • private static int state,这种同步状态会导致可见性问题;
  • private volatile static int state,操作state ++不是原子操作。
方法 2:同步状态 + synchronized

如果要使用private static int state类型的同步状态,我们可以引入synchronized,代码如下:

    private static int state = 1;
    
    private static final Object OBJ_LOCK = new Object();
    
    private static void useSynchronized() {
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          synchronized (OBJ_LOCK) {
            while (state % 3 == 1) {
              System.out.print("A,");
              state++;
              i++;
            }
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          synchronized (OBJ_LOCK) {
            while (state % 3 == 2) {
              System.out.print("B,");
              state++;
              i++;
            }
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; ) {
          synchronized (OBJ_LOCK) {
            while (state % 3 == 0) {
              System.out.print("C");
              System.out.println();
              state++;
              i++;
            }
          }
        }
      }).start();
    }

Tips:synchronized保证了修饰代码块中内容的可见性和原子性。

方法 3:同步状态 + synchronized + Object#wait与Object#notifyAll

以上方法的问题是,每个线程都会执行大量的“空转”,state 不满足进入 while 循环的情况,变量 i 尚未发生变化时,导致 for 循环成为“死循环”。通过引入等待Object#wait与唤醒Object#notifyAll来减少“空转”的次数,代码如下:

    private static int state = 1;
    
    private static final Object OBJ_LOCK = new Object();
    
    private static void useSynchronizedWithNotify() {
      new Thread(() -> {
        synchronized (OBJ_LOCK) {
          for (int i = 0; i < 100; i++) {
            while (state % 3 != 1) {
              try {
                OBJ_LOCK.wait();
              } catch (InterruptedException e) {
                throw new RuntimeException(e);
              }
            }
            System.out.print("A,");
            state++;
            OBJ_LOCK.notifyAll();
          }
        }
      }).start();
      
      new Thread(() -> {
        synchronized (OBJ_LOCK) {
          for (int i = 0; i < 100; i++) {
            while (state % 3 != 2) {
              try {
                OBJ_LOCK.wait();
              } catch (InterruptedException e) {
                throw new RuntimeException(e);
              }
            }
            System.out.print("B,");
            state++;
            OBJ_LOCK.notifyAll();
          }
        }
      }).start();
      
      new Thread(() -> {
        synchronized (OBJ_LOCK) {
          for (int i = 0; i < 100; i++) {
            while (state % 3 != 0) {
              try {
                OBJ_LOCK.wait();
              } catch (InterruptedException e) {
                throw new RuntimeException(e);
              }
            }
            System.out.print("C");
            System.out.println();
            state++;
            OBJ_LOCK.notifyAll();
          }
        }
      }).start();
    }
方法 4:同步状态 + ReentrantLock + Condition

与方法 3 基本一致,ReentrantLock 代替synchronized,Condition#awaitCondition#signalAll代替Object#waitObject#notifyAll,代码如下:

    private static int state = 1;
    
    private static final ReentrantLock REENTRANT_LOCK = new ReentrantLock();
    
    private static final Condition CONDITION = REENTRANT_LOCK.newCondition();
    
    private static void useReentrantLockWithCondition() {
      new Thread(() -> {
        for (int i = 0; i < 100; i++) {
          try {
            REENTRANT_LOCK.lock();
            while (state % 3 != 1) {
              CONDITION.await();
            }
            System.out.print("A,");
            state++;
            CONDITION.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            REENTRANT_LOCK.unlock();
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; i++) {
          try {
            REENTRANT_LOCK.lock();
            while (state % 3 != 1) {
              CONDITION.await();
            }
            System.out.print("B,");
            state++;
            CONDITION.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            REENTRANT_LOCK.unlock();
          }
        }
      }).start();
      
      new Thread(() -> {
        for (int i = 0; i < 100; i++) {
          try {
            REENTRANT_LOCK.lock();
            while (state % 3 != 1) {
              CONDITION.await();
            }
            System.out.print("C");
            System.out.println();
            state++;
            CONDITION.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            REENTRANT_LOCK.unlock();
          }
        }
      }).start();
    }

Tips:除了以上 4 中方法外,还可以通过其它的同步工具实现,如:Semphore,CountDownLatch,CyclicBarrier 等。

参考资料:关于线程你必须知道的8个问题(中),详解AQS家族的成员:Semaphore,详解AQS家族的成员:CountDownLatch,AQS家族的“外门弟子”:CyclicBarrier


使用4个线程交替打印数字1~100。

上一题的翻版,我这里只演示 ReentrantLock 的实现方式,代码如下:

    static int state = 1;
    
    static ReentrantLock reentrantLock = new ReentrantLock();
    
    static Condition condition = reentrantLock.newCondition();
    
    public static void main(String[] args) {
      new Thread(() -> {
        for (int i = 0; i < 25; i++) {
          try {
            reentrantLock.lock();
            if (state % 4 != 1) {
              condition.await();
            }
            System.out.println("Thread-1 :" + state);
            state++;
            condition.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            reentrantLock.unlock();
          }
        }
      }).start();
    
      new Thread(() -> {
        for (int i = 0; i < 25; i++) {
          try {
            reentrantLock.lock();
            if (state % 4 != 2) {
              condition.await();
            }
            System.out.println("Thread-2 :" + state);
            state++;
            condition.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            reentrantLock.unlock();
          }
        }
      }).start();
    
      new Thread(() -> {
        for (int i = 0; i < 25; i++) {
          try {
            reentrantLock.lock();
            if (state % 4 != 3) {
              condition.await();
            }
            System.out.println("Thread-3 :" + state);
            state++;
            condition.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            reentrantLock.unlock();
          }
        }
      }).start();
    
      new Thread(() -> {
        for (int i = 0; i < 25; i++) {
          try {
            reentrantLock.lock();
            if (state % 4 != 0) {
              condition.await();
            }
            System.out.println("Thread-4 :" + state);
            state++;
            condition.signalAll();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          } finally {
            reentrantLock.unlock();
          }
        }
      }).start();
    }

参考资料:关于线程你必须知道的8个问题(中)


如保证 3 个线程按照执行的顺序执行?

可以使用Thread#join方法,在线程中启动另一个线程,同时阻塞当前线程,代码如下:

    Thread t1 = new Thread(()-> System.out.println("线程[t1]执行!"));
    Thread t2 = new Thread(()-> {
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("线程[t2]执行!");
    });
    Thread t3 = new Thread(()-> {
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("线程[t3]执行!");
    });
    
    t3.start();
    t2.start();
    t1.start();

Thread#join方法的作用是,阻塞执行线程,等待调用 join 方法的线程执行完毕。如:在上述代码中,t2 中执行t1.join(),那么线程 t2 就需要等待线程 t1 执行完毕后再执行。

参考资料:关于线程你必须知道的8个问题(中)


如何实现一个线程执行完毕后执行另一个线程

面试公司:美团

参考“如保证 3 个线程按照执行的顺序执行?”的解答。

参考资料:关于线程你必须知道的8个问题(中)


如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核技术的金融摸鱼侠王有志,我们下次再见!

延伸 · 阅读

精彩推荐