> 文档中心 > Java 多线程—线程通讯【线程通知与等待,wait / notify() / notifyAll()】

Java 多线程—线程通讯【线程通知与等待,wait / notify() / notifyAll()】

线程通讯

  • 1. wait() 函数
  • 2. wait(long timeout)函数
    • 补充知识点
  • 3. notify() 函数
  • 4. notifyAll() 函数
  • 5. 面试问题:wait vs sleep
  • 6. LockSupport

由于线程之间是抢占式执行的,因此线程之间的执行的先后顺序难以预知,但是实际的开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。线程通讯 就是通过实现多线程之间的交互,让线程的每一个独立的“执行流”通过交互相互配合,在能够有效避免线程并发和死锁的的问题下发挥多线程的优势,更高效的处理一些时间片短,任务量大的执行任务。

在 Java 当中,实现线程之间进行通讯和配合执行的功能有三个方法

  • wait() / wait(long timeout):让线程进入等待状态。
  • notify():唤醒当前对象上一个休眠的线程。
  • notifyAll():唤醒当前对象上的所有线程。

如图:以上三个操作就是对 Java监视器中的 WaitSet 集合的放入和取出操作。
在这里插入图片描述
注意事项:以上三个方法都时对象级别的方法不是锁级别的方法,而且都必须配合 synchronized() 方法使用,因为只有是在获取了锁的前提下执行此线程,我们才可以在执行过程中对这个锁进行有必要的释放-wait(),有选择的拿回-notify() ~
Java监视器其他相关问题-synchronized()

1. wait() 函数

当一个线程调用一个共享变量 wait() 方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:(1)线程调用了该共享对象 notify() 或者 notifyAll() 方法,这样能通过调用方法进行唤醒也就是实现线程通讯了~(线程通讯是形象表达,有时候在面试时会被提到这个名词,我们就要知道面试官想要问的就是现成的通知和等待了) (2)其他线程调用了该线程 interrupt() 方法 线程中断 interrupt(),该线程抛出 InterruptedException 异常返回。

  • wait() 使用

wait 执行流程:

  1. 使当前执行代码的线程进行等待
    【把线程放入等待队列中,暂停它的执行】
  2. 释放当前线程获取的锁
    【因为调用wait方法默认不传参是无限等待的,如果不释放锁的话,就会在他等待的时候其他线程也无法获取这个锁进行执行,就会造成资源的浪费和线程的无限阻塞】
  3. 满足一定条件时被唤醒,重新尝试获取这个锁
    【在调用notify或者notifyAll时会从WaitSet集合中把线程唤醒起来,继续去尝试获取锁继续执行此线程】
public class WaitDemo_线程通讯 {    public static void main(String[] args) { Object obj1 = new Object(); Thread t = new Thread(()->{     System.out.println("线程1开始执行");     try {  synchronized (obj1){      System.out.println("线程1调用wait方法....");      // 无限等待状态      obj1.wait();  }     } catch (InterruptedException e) { //接收到一个Interrupter异常  e.printStackTrace();     }     System.out.println("线程1执行完成"); },"线程1"); t.start();    }}

在这里插入图片描述
如上代码是经典的调用共享变量 wait() 方法的实例,首先通过同步块获取 obj 上面的监视器锁,然后在 while 循环内调用 obj 的 wait() 方法。

另外需要注意的是,当前线程调用共享变量的 wait() 方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。

public class WaitDemo_线程通讯2 {    public static void main(String[] args) throws InterruptedException { Object obj1 = new Object(); Object obj2 = new Object(); Thread t1 = new Thread(() -> {     System.out.println("线程1开始执行");     try {  synchronized (obj1) {      System.out.println("线程1得到obj1锁");      synchronized (obj2) {   System.out.println("线程1得到obj2锁");   System.out.println("线程1调用obj1对象的wait方法....");   // 无限等待状态   obj1.wait();      }  }     } catch (InterruptedException e) { //接收到一个Interrupter异常  e.printStackTrace();     }     System.out.println("线程1执行完成"); }, "线程1"); Thread t2 = new Thread(() -> {     try {  Thread.sleep(1000);  System.out.println("线程2开始执行");  synchronized (obj1) {      System.out.println("线程2得到obj1锁");      System.out.println("线程2尝试获取obj2锁");      synchronized (obj2) {   System.out.println("线程2得到obj2锁");      }  }  System.out.println("线程2执行完成");     } catch (InterruptedException e) {  e.printStackTrace();     } }, "线程2"); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("main over");    }}

输出结果如下:
在这里插入图片描述
如上代码,在 main 函数里面启动了线程1 和线程2 ,为了让线程1 先获取到锁,这里让线程2 先休眠了 1s,线程1 先后获取到共享变量 obj1 和共享变量 obj2 上的锁,然后调用了 obj1 的 wait() 方法阻塞自己,此时也就会释放掉获取的 obj1 上的锁。

线程2 休眠结束后会首先尝试获取 obj1 上的锁,如果当时线程1 还没有调用 wait() 方法释放该锁,那么线程2 就会被阻塞,当线程1 释放了 obj1 上的锁后,线程2 就会获取到 obj1 上的锁,然后尝试获取 obj2 上的锁。由于线程1 调用的是 obj1 上的 wait() 方法,所以线程1 挂起自己后并没有释放获取到的 obj2 上的锁,所以线程2 尝试获取 obj2 上的锁时会被阻塞。

这就证明了当前线程调用共享对象的 wait() 方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。

最后再举一个例子进行说明 。当一个线程调用共享对象的 wait() 方法被阻塞挂起后,如果其他线程中断了该线程, 线程会抛出 InterruptedException 异常并返回。

public class WaitDemo_线程通讯3 {    public static void main(String[] args) throws InterruptedException { Object obj1 = new Object(); Thread t = new Thread(()->{     System.out.println("线程1开始执行");     try {  synchronized (obj1){      System.out.println("线程1调用wait方法....");      // 无限等待状态      obj1.wait();  }     } catch (InterruptedException e) { //接收到一个Interrupter异常  e.printStackTrace();     }     System.out.println("线程1执行完成"); },"线程1"); t.start(); Thread.sleep(1000); System.out.println("在main线程中调用对象线程1的interrupt方法"); t.interrupt(); System.out.println("调用interrupt方法成功终止了线程1的执行");    }}

在这里插入图片描述
在如上代码中,线程1 调用共享对象 obj 的wait() 方法后阻塞挂起了自己,然后主线程在休眠 1s 后中断了线程1 ,中断后 thread1 在obj1.wait() 处抛出 java.lang.InterruptedException 异常而返回并终止。

2. wait(long timeout)函数

该方法相比 wait() 方法多了一个超时参数,它的不同之处在于,如果一个线程调用共
享对象的该方法挂起后,没有在指定的timeout ms 内被其他线程调用该共享变量的
notify() 或者 notifyAll() 方法唤醒,那么该函数还是会因为超时而返回。如果将 timeout置为 0 则和 wait 方法效果一样,因为在 wait() 方法内部就是调用了 wait(0)。需要注意的是,如果在调用该函数时,传递了一个负的 timeout 则会抛出 IllegalArgumentException 异常。

补充知识点

使用无参的 wait 方法,线程会进入 WAITING 状态;
使用有参的 wait 方法,线程会进入 TIMED_WAITING 状态。

在这里插入图片描述

3. notify() 函数

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

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

public class WaitThread {    public static void main(String[] args) { Object lock = new Object(); Object lock2 = new Object(); Thread t1 = new Thread(() -> {     System.out.println("线程1开始执行");     try {  synchronized (lock) {      System.out.println("线程1调用wait方法....");      // 无限期的等待状态      lock.wait();      System.out.println("线程1:恢复执行之后又进入休眠状态");      Thread.sleep(2000);      System.out.println("线程1执行完成");  }     } catch (InterruptedException e) {  e.printStackTrace();     } }, "线程1"); Thread t2 = new Thread(() -> {     System.out.println("线程2开始执行");     try {  synchronized (lock) {      System.out.println("线程2调用wait方法....");      // 无限期的等待状态      lock.wait();      System.out.println("线程2:恢复执行之后又进入休眠状态");      Thread.sleep(2000);      System.out.println("线程2执行完成");  }     } catch (InterruptedException e) {  e.printStackTrace();     } }, "线程2"); Thread t3 = new Thread(() -> {     System.out.println("线程3开始执行");     try {  synchronized (lock) {      System.out.println("线程3调用wait方法....");      // 无限期的等待状态      lock.wait();      System.out.println("线程3:恢复执行之后又进入休眠状态");      Thread.sleep(2000);      System.out.println("线程3执行完成");  }     } catch (InterruptedException e) {  e.printStackTrace();     } }, "线程3"); t1.start(); t2.start(); t3.start(); // 唤醒 lock 对象上休眠的线程的(随机唤醒一个) Thread t4 = new Thread(() -> {     try {  Thread.sleep(1500);     } catch (InterruptedException e) {     }     System.out.println("线程4:开始执行");     synchronized (lock) {  // 发出唤醒通知  lock.notify();  System.out.println("线程4:执行了唤醒操作");  try {      Thread.sleep(2000);  } catch (InterruptedException e) {  }  System.out.println("线程4:synchronized 执行完了");     } }, "线程4"); t4.start();    }}

执行结果:
在这里插入图片描述
notify() 随机唤醒一个线程继续执行,但试能唤醒一个线程。

4. notifyAll() 函数

不同于在共享变量上调用 notify() 函数会唤醒被阻塞到该共享变量上一个线程,notifyAll() 方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。
在这里插入图片描述
将以上代码在线程4 中调用的唤醒函数改为 notifyAll() ,而后被 wait 阻塞的所有线程都被唤醒继续执行啦~

5. 面试问题:wait vs sleep

  1. wait(0) VS sleep(0)
    wait(0) : 线程无限期等待下去
    sleep(0) : 相当于 Thread.yeild(),让出 CPU 执行权,但是 sleep(0) 会继续执行。

  2. wait 和 sleep 在有锁的情况下对锁的处理行为完全不同

import java.util.concurrent.TimeUnit;/** * wait 和 sleep 释放锁行为的区别 */public class WaitSleepDemo {    public static void main(String[] args) throws InterruptedException { Object lock = new Object(); Object lock2 = new Object(); Thread t1 = new Thread(() -> {     synchronized (lock) {  System.out.println("线程1:开始执行");  try {      lock.wait(3 * 1000);  } catch (InterruptedException e) {      e.printStackTrace();  }  System.out.println("线程1:结束执行");     } }, "wait"); t1.start(); Thread t2 = new Thread(() -> {     synchronized (lock2) {  System.out.println("线程2:开始执行");  try {      Thread.sleep(3 * 1000);  } catch (InterruptedException e) {      e.printStackTrace();  }  System.out.println("线程2:结束执行");     } }, "sleep"); t2.start(); // 创建 2 个线程,先让线程休眠 1s 之后,尝试获取,看能不能获取到锁 // 如果可以获取到锁,说明休眠时线程是释放锁的,而如果获取不到锁,说明是不释放锁 Thread t3 = new Thread(() -> {     try {  TimeUnit.SECONDS.sleep(1);     } catch (InterruptedException e) {  e.printStackTrace();     }     System.out.println("尝试获取 wait 方法的锁");     synchronized (lock) {  System.out.println("成功获取 wait 的锁");     } }, "wait2"); t3.start(); Thread t4 = new Thread(() -> {     try {  TimeUnit.SECONDS.sleep(0);     } catch (InterruptedException e) {  e.printStackTrace();     }     System.out.println("尝试获取 sleep 方法的锁");     synchronized (lock2) {  System.out.println("成功获取 sleep 的锁");     } }, "sleep2"); t4.start();    }}

执行结果:
在这里插入图片描述
从以上代码,我们首先创建两个线程,分别给线程加锁之后在线程1 中用 wait 阻塞,在线程2 中用 sleep 休眠,从结果我们可以看到,新线程想要获取 线程1 的锁,在线程1 被 wait 阻塞之后就可以立刻获取到这个锁;而新线程不可以直接获取到线程2 被 sleep 休眠线程的锁,只有在休眠时间结束后线程2 执行完毕才能去尝试获取。

  • 小结

  • 相同点:

  1. 两种方法都可以让线程进入休眠状态。
  2. 都可以相应一个 Interrupt() 中断请求。
  • 不同点
  1. wait 必须配合 synchronized 使用,而 sleep 不需要。
  2. wait 属于 Object(对象)的方法;而 sleep 属于 Thread(线程)的方法。
  3. sleep 不会释放锁;而 wait 他会释放掉锁。
  4. sleep 必须要传递一个数值类型的参数;而 wait 可以不传参。
  5. sleep 让线程进入到 TIME_WAITING 状态;而无参的 wait 方法让线程进入了 WAITING 状态。
  6. 一般情况下 sleep 只能等待超时时间之后再恢复执行;而 wait 可以接收 notify/notifyAll 之后继续执行。

6. LockSupport

我们通过以上了解,引出了这样一个问题:先看如下代码

public static void main(String[] args) throws InterruptedException { Object lock = new Object(); Thread t1 = new Thread(new Runnable() {     @Override     public void run() {  synchronized (lock) {      try {   lock.wait();     } catch (InterruptedException e) {   e.printStackTrace();     }      System.out.println("t1"); }    }}); Thread t2 = new Thread(new Runnable() {     @Override     public void run() {  synchronized (lock) {      try {   lock.wait();     } catch (InterruptedException e) {   e.printStackTrace();     }      System.out.println("t2"); }    }}); Thread t3 = new Thread(new Runnable() {     @Override     public void run() {  synchronized (lock) {      try {   lock.wait();     } catch (InterruptedException e) {   e.printStackTrace();     }      System.out.println("t3"); }    }}); t1.start(); t2.start(); t3.start(); Thread.sleep(100); synchronized (lock) {     lock.notify();} Thread.sleep(100); synchronized (lock) {     lock.notify();} Thread.sleep(100); synchronized (lock) {     lock.notify();}   }}

执行结果:
在这里插入图片描述
我们看到的现象是,每次我们去唤醒线程时 notify 所进行唤醒的线程是随机的,那么我们如果想要实现对线程的顺序唤醒,也就是第一次唤醒的线程是第一次休眠的线程,用 wait/notify 这种方法就不可取了。 下面我用另外一篇博客详细介绍了 线程休眠和指定唤醒:LockSupport 方法,大家有需要可以点解以下链接跳转阅读。
LockSupport 关于线程的休眠与唤醒工具类