Java
Java多线程

Java多线程 03 - 线程等待与唤醒

简介:在Object.java中,定义了wait()、notify()和notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()则是用于唤醒当前对象上的等待线程。

1. wait()、notify()、notifyAll()等方法介绍

在Object.java中,定义了wait()notify()notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时wait()也会让当前线程释放它所持有的锁。而notify()notifyAll()则是用于唤醒当前对象上的等待线程,notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。

Object类中关于等待/唤醒的API详细信息如下:

  • notify():唤醒在此对象监视器上等待的单个线程。
  • notifyAll():唤醒在此对象监视器上等待的所有线程。
  • wait():让当前线程处于等待(阻塞)状态,直到其他线程调用此对象的notify()方法或notifyAll()方法,当前线程被唤醒(进入就绪状态)。
  • wait(long timeout):让当前线程处于等待(阻塞)状态,直到其他线程调用此对象的notify()方法或notifyAll()方法,或者超过指定的时间量,当前线程被唤醒(进入就绪状态)。
  • wait(long timeout, int nanos):让当前线程处于等待(阻塞)状态,直到其他线程调用此对象的notify()方法或notifyAll()方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量,当前线程被唤醒(进入就绪状态)。

2. wait()和notify()示例

下面通过示例演示wait()notify()配合使用的情形。

  • class TestThread implements Runnable {
  • public TestThread() {
  • System.out.println("TestThread init");
  • }
  • @Override
  • public void run() {
  • System.out.println(Thread.currentThread().getName() + " entered run method");
  • synchronized (this) {
  • System.out.println(Thread.currentThread().getName() + " inoked synchronized code block");
  • try {
  • for (int i = 0; i < 3; i++) {
  • Thread.sleep(300);
  • System.out.println(Thread.currentThread().getName() + " invoked " + i);
  • }
  • // 唤醒以testThread对象为锁的正在等待的主线程,将进入就绪状态
  • this.notify();
  • // 此时主线程还在就绪状态
  • System.out.println(Thread.currentThread().getName() + " will end synchronized code block");
  • Thread.sleep(1000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • }
  • }
  • }
  • public class WaitTest {
  • public static void main(String[] args) {
  • TestThread testThread = new TestThread();
  • synchronized (testThread) {
  • Thread thread = new Thread(testThread);
  • /**
  • * 此时虽然thread启动了,但是由于testThread对象锁在主线程手中,
  • * 且testThread对象的run方法中有以this(即testThread自身)为锁的同步代码块,
  • * 所以testThread会在同步代码块之前阻塞
  • */
  • thread.start();
  • try {
  • for (int i = 0; i < 3; i++) {
  • Thread.sleep(300);
  • System.out.println(Thread.currentThread().getName() + " invoked " + i);
  • }
  • System.out.println(Thread.currentThread().getName() + " begin wait");
  • // 进行等待,即拥有testThread对象锁的线程转入等待阻塞状态
  • testThread.wait();
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • System.out.println(Thread.currentThread().getName() + " end wait");
  • }
  • }
  • }

多次运行得到的打印一致的结果如下:

  • TestThread init
  • Thread-0 entered run method
  • main invoked 0
  • main invoked 1
  • main invoked 2
  • main begin wait
  • Thread-0 inoked synchronized code block
  • Thread-0 invoked 0
  • Thread-0 invoked 1
  • Thread-0 invoked 2
  • Thread-0 will end synchronized code block
  • main end wait

注:需要注意的是,wait()方法和notify()方法都是使用“锁对象”来调用的。在锁对象上调用wait()方法,会使当前拿到锁对象的线程进入等待(阻塞)状态,其他因当前锁对象阻塞的线程将获取CPU资源转入就绪状态;在当前锁对象上调用notify()方法,会将其他正处于阻塞等待状态的线程唤醒,进入锁定(阻塞)状态,当获取到正在运行的线程释放锁对象并争夺到CPU资源后将转入运行状态。

这里对上面运行的结果进行解释:

  1. 在main方法中,首先创建了一个TestThread对象testThread,因此会打印TestThread init
  2. 主线程获得testThread对象锁,进入同步代码块,创建thread线程并启动它,但由于thread线程的run()方法中也存在以testThread对象为锁的同步代码块,因此只会打印Thread-0 entered run method,然后阻塞在第10行。
  3. 主线程循环打印三句信息和main begin wait后,会调用testThread.wait(),这句代码会使主线程进入等待(阻塞)状态,并释放了testThread对象锁。
  4. thread线程获取到testThread对象锁,从第10行开始继续执行,进入同步代码块,打印了Thread-0 inoked synchronized code block和三句循环语句之后,调用了this.notify(),这句代码会使唤醒阻塞的主线程,让其进入锁定(阻塞)状态,等待获取testThread对象锁。由于此时testThread对象锁还在thread线程手中,它会继续往下执行,打印Thread-0 will end synchronized code block后结束同步代码块的运行,交出testThread对象锁。
  5. 主线程获取testThread对象锁后,进入就绪状态,当获取到CPU资源会继续往下执行,因此会打印main end wait

两个线程的交替运行时序图如下:

1.wait和notify.png

3. wait(long timeout)和notify()示例

wait(long timeout)会让当前线程处于等待(阻塞)状态,“直到其他线程调用此对象的notify()方法或notifyAll()方法,或者超过指定的时间量,当前线程被唤醒(进入就绪状态)。下面的示例就是演示wait(long timeout)在超时情况下,线程被唤醒的情况:

  • class TestThread implements Runnable {
  • public TestThread1() {
  • System.out.println("TestThread init");
  • }
  • @Override
  • public void run() {
  • System.out.println(Thread.currentThread().getName() + " entered run method");
  • while (true) {
  • ;
  • }
  • }
  • }
  • public class WaitTest {
  • public static void main(String[] args) {
  • TestThread testThread = new TestThread();
  • synchronized (testThread) {
  • Thread thread = new Thread(testThread);
  • /**
  • * 此时虽然thread启动了,但是由于testThread对象锁在主线程手中,
  • * 且testThread对象的run方法中有以this(即testThread自身)为锁的同步代码块,
  • * 所以testThread会在同步代码块之前阻塞
  • */
  • thread.start();
  • try {
  • for (int i = 0; i < 3; i++) {
  • Thread.sleep(300);
  • System.out.println(Thread.currentThread().getName() + " invoked " + i);
  • }
  • System.out.println(Thread.currentThread().getName() + " begin wait");
  • // 主线程进入等待,超时3000毫秒后转入锁定(阻塞)状态
  • testThread.wait(3000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • System.out.println(Thread.currentThread().getName() + " end wait");
  • }
  • }
  • }

运行结果如下:

  • TestThread init
  • Thread-0 entered run method
  • main invoked 0
  • main invoked 1
  • main invoked 2
  • main begin wait
  • main end wait

打印出结果后程序不会结束,因为thread线程一直在死循环运行。

wait(long timeout)方法超时后

当前拿到“锁对象”的线程在调用wait(long timeout)方法,会进入等待(阻塞)状态,其他因当前锁对象阻塞的线程将获取CPU资源转入运行状态;在当前线程等待时间超时后,会进入就绪状态;或者当其他线程在锁对象上调用notify()方法,也会唤醒当前线程进入就绪状态。

这里对上面运行的结果进行解释:

  1. 在main方法中,首先创建了一个TestThread对象testThread,因此会打印TestThread init
  2. 主线程获得testThread对象锁,进入同步代码块,创建thread线程并启动它,thread线程的run()方法中会打印Thread-0 entered run method,并死循环运行。
  3. 主线程循环打印三句信息和main begin wait后,会调用testThread.wait(3000),这句代码会使主线程进入等待(阻塞)状态,并释放了testThread对象锁。
  4. thread线程持续循环运行,当超过3000ms之后,等待超时,会唤醒阻塞的主线程,让其进入锁定(阻塞)状态。
  5. 主线程获取testThread对象锁后,继续往下运行,打印main end wait

两个线程的交替运行时序图如下:

2.带超时的wait和notify.png

4. wait()和notifyAll()

通过前面的示例,可以得知notify()可以唤醒在此对象监视器上等待的单个线程,而notifyAll()的作用是唤醒在此对象监视器上等待的所有线程。有下面的示例代码:

  • class TestThread implements Runnable {
  • // 锁对象
  • private Object lock;
  • public TestThread(Object lock) {
  • // 在创建TestThread传入锁对象
  • this.lock = lock;
  • System.out.println("TestThread init");
  • }
  • @Override
  • public void run() {
  • // 使用传入的锁对象构建synchronized代码块
  • synchronized (this.lock) {
  • System.out.println(Thread.currentThread().getName() + " entered run method");
  • try {
  • System.out.println(Thread.currentThread().getName() + " begin wait");
  • // 进入等待状态
  • this.lock.wait();
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • System.out.println(Thread.currentThread().getName() + " end wait");
  • }
  • }
  • }
  • public class WaitTest {
  • public static void main(String[] args) {
  • // 共享的锁对象
  • WaitTest lock = new WaitTest();
  • // 使用共享的锁对象开启三个线程并启动
  • new Thread(new TestThread(lock)).start();
  • new Thread(new TestThread(lock)).start();
  • new Thread(new TestThread(lock)).start();
  • try {
  • System.out.println(Thread.currentThread().getName() + " begin sleep");
  • // 主线程等待5秒
  • Thread.sleep(5000);
  • } catch (InterruptedException e) {
  • e.printStackTrace();
  • }
  • // 使用共享锁对象构建synchronized代码块
  • synchronized (lock) {
  • System.out.println(Thread.currentThread().getName() + " notify all");
  • // 唤醒所有等待的线程
  • lock.notifyAll();
  • System.out.println(Thread.currentThread().getName() + " continue exeucted");
  • }
  • }
  • }

某次运行的结果如下:

  • TestThread init
  • TestThread init
  • TestThread init
  • Thread-0 entered run method
  • Thread-0 begin wait
  • Thread-1 entered run method
  • Thread-1 begin wait
  • Thread-2 entered run method
  • Thread-2 begin wait
  • main begin sleep
  • main notify all
  • main continue exeucted
  • Thread-2 end wait
  • Thread-1 end wait
  • Thread-0 end wait
notifyAll()方法针对的线程

调用notifyAll()方法的对象必须是锁对象,会唤醒以此锁对象进入等待状态的线程。

这里对上面运行的结果进行解释:

  1. 在main方法中,首先创建了一个WaitTest对象lock作为共享锁。
  2. 然后创建三个TestThread对象,将第1步中创建的共享锁传入,三个线程都在run()方法中创建以此共享锁为锁对象的synchronized代码块,并在其中调用this.lock.wait()进入等待(阻塞)状态。因此会打印第1 ~ 9行。
  3. 主线程调用Thread.sleep(5000)进入睡眠状态,会打印main begin sleep。睡眠结束后,进入以第1步中创建的lock为锁对象的synchronized代码块,会打印main notify all,然后在其中调用lock.notifyAll()方法唤醒所有以lock为锁对象的等待线程。
  4. 虽然前面的三个等待的线程被唤醒了,但是此时lock锁对象还在主线程手中,所以主线程会继续执行,打印main continue exeucted,然后释放锁对象。
  5. 前面的三个等待的线程争抢锁对象,并继续执行,因此有了最后三行的打印。

两个线程的交替运行时序图如下:

3.notifyall.png

5. 为什么notify()、wait()等函数定义在Object中,而不是Thread中?

Object中的wait()notify()等函数,和synchronized一样,会对对象的同步锁进行操作。

wait()会使当前线程等待,因为线程进入等待状态,所以线程应该释放它锁持有的同步锁,否则其它线程获取不到该同步锁而无法运行。线程调用wait()之后,会释放它锁持有的同步锁;而且,根据前面的介绍,我们知道,等待线程可以被notify()notifyAll()唤醒。现在,请思考一个问题:notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据对象的同步锁。

负责唤醒等待线程的那个线程(我们称为唤醒线程),它只有在获取该对象的同步锁(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()notifyAll()方法之后,才能唤醒等待线程。虽然等待线程被唤醒,但是它不能立刻执行,因为调用notify()notifyAll()的唤醒线程还持有该对象的同步锁。必须等到该唤醒线程释放了对象的同步锁之后,等待线程才能获取到对象的同步锁进而继续运行。

总之,notify()wait()依赖于同步锁,而同步锁是对象锁持有,并且每个对象有且仅有一个。这就是为什么notify()wait()等函数定义在Object类,而不是Thread类中的原因。

注:只有wait()没有notify()的情况。如果程序中只有两个线程在执行,且竞争同一个同步锁,其中A线程因调用wait()进入等待状态了,B线程继续执行,就算其他的地方并没有对处于等待状态的A线程进行唤醒,在B线程执行完后,A线程也会结束等待而继续执行,因为此时没有其他线程与A线程竞争同步锁了。执行wait()进入等待状态的线程,有下面5种唤醒方式:
1. 通过notify()唤醒。
2. 通过notifyAll()唤醒。
3. 通过interrupt()中断唤醒。
4. 如果是通过调用wait(long timeout)进入等待状态的线程,当时间超时的时候,也会被唤醒。
5. 没有其它线程与该线程竞争同步锁,该线程也会被唤醒。

6. 为什么wait()一定要放在循环中

在多线程的编程实践中,wait()的使用方法如下:

  • synchronized (monitor) {
  • // 判断条件谓词是否得到满足
  • while(!locked) {
  • // 等待唤醒
  • monitor.wait();
  • }
  • // 处理其他的业务逻辑
  • }

那为什么非要while判断,而不采用if判断呢?如下:

  • synchronized (monitor) {
  • // 判断条件谓词是否得到满足
  • if(!locked) {
  • // 等待唤醒
  • monitor.wait();
  • }
  • // 处理其他的业务逻辑
  • }

这是因为,如果采用if判断,当线程从wait中唤醒时,那么将直接执行处理其他业务逻辑的代码,但这时候可能出现另外一种可能,条件谓词已经不满足处理业务逻辑的条件了,从而出现错误的结果,于是有必要进行再一次判断,如下:

  • synchronized (monitor) {
  • // 判断条件谓词是否得到满足
  • if(!locked) {
  • // 等待唤醒
  • monitor.wait();
  • if(locked) {
  • // 处理其他的业务逻辑
  • } else {
  • // 跳转到monitor.wait();
  • }
  • }
  • }

而循环则是对上述写法的简化,唤醒后再次进入while条件判断,避免条件谓词发生改变而继续处理业务逻辑的错误。

注:执行wait()进入等待状态的线程可能会出现“假醒”的情况,所以一般还需要添加另外的限定条件来重新使“假醒”线程进入阻塞状态。