在之前的线程系列文章中,我们介绍了synchronized和volatile关键字,使用它能解决线程同步的问题,但是它们无法解决线程之间协调和通信的问题。
举个简单的例子,比如线程 A 负责将 int 型变量 i 值累加操作到 10000,然后通知线程 B 负责把结果打印出来。
这个怎么实现呢?其中一个最简单的办法就是,线程 B 不断的通过轮询方式while(i == 10000)检查是否满足条件,这样就可以实现了。
虽然这种方式可以实现需求,但是也带来了另一个问题:线程 B 中的while()操作不会释放 CPU 资源,会导致 CPU 一直在这个方法上做判断操作,极大的浪费 CPU 资源。
我们知道 CPU 资源是非常非常昂贵的,因为使用 CPU 资源不只是当前一个应用程序,还有其它许许多多的应用程序。如果把这些轮询的时间释放出来,给别的线程使用,更能显著提升应用程序的运行效率。比如,线程 A 操作完成之后,通知线程 B 进行后续的操作,线程 B 无需通过轮询检查的方式来完成线程之间的协调,这样是不是更好。
在 Java 的父类中,也就是Object类中,就有三个方法:wait()、notify()、notifyAll(),它们就可以实现线程之间的通信。
如果没有接触多线程,这些方法可能基本上使用不到。下面我们一起来看看它们的使用方式!
wait()
wait()方法,顾名思义,表示等待的意思,它的作用是:使执行当前代码的线程进入阻塞状态,将当前线程置入"预执行队列"中,并且wait()所在的代码处停止执行,直到接到通知或被中断。
不过有个前提,在调用wait()方法之前,线程必须获得该对象的锁,因此只能在synchronized修饰的同步方法/同步代码块中调用wait()方法;同时,wait()方法执行后,会立即释放获得的对象锁以便其它线程使用,当前线程被阻塞,进入等待状态。
至于wait()为什么有阻塞的效果,其内部机制非常复杂,主要由 JVM 的 C 代码实现,大家了解就行。
notify()
notify()方法,顾名思义,表示通知的意思,它的作用是:让处于同一监视器下的等待线程被重新唤醒,如果有多个线程等待,那么随机挑选出一个等待的线程,对其发出通知notify(),并使它等待获取该对象的对象锁。
注意“等待获取该对象的对象锁”,这意味着即使收到了通知,等待的线程也不会马上获取对象锁,必须等待notify()方法的线程释放锁才可以。
调用环境和wait()一样,notify()也要在synchronized修饰的同步方法/同步代码块中调用。
notifyAll()
notifyAll()方法,顾名思义,也是表示通知的意思,它的作用是:让所有处于同一监视器下的等待线程被重新唤醒,notify()方法只会随机的唤醒一个线程,而使用notifyAll()方法将一次性全部唤醒。
通常来说,notifyAll()方法更安全,因为当我们的代码逻辑考虑不周的时候,使用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。
调用环境和notify()一样,notifyAll()也要在synchronized修饰的同步方法/同步代码块中调用。
三个方法总结下来就是:
1.wait()方法,使线程阻塞,进入等待状态
2.notify()方法,唤醒处于等待的线程,如果有多个线程就随机从中取一个
3.notifyAll()方法,唤醒所有处于等待的线程
通常wait()方法,一般与notify()或者notifyAll()搭配使用比较多。
下面我们看一个简单的示例。
publicclass MyThreadA extends Thread{ private Objectlock;publicMyThreadA(Objectlock){ this.lock=lock;}@Overridepublicvoid run(){ synchronized(lock){ System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" wait begin");try {// 进入阻塞等待lock.wait();} catch(Exception e){ e.printStackTrace();} System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" wait end");} } }
publicclass MyThreadB extends Thread{ private Objectlock;publicMyThreadB(Objectlock){ this.lock=lock;}@Overridepublicvoid run(){ synchronized(lock){ System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" notify begin");// 唤醒其它等待线程lock.notify();System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" notify end");} } }
publicclass MyThreadTest {publicstatic void main(String[]args)throws InterruptedException { Objectlock=new Object();MyThreadA threadA=new MyThreadA(lock);threadA.start();//过3秒再启动下一个线程Thread.sleep(3000);MyThreadB threadB=new MyThreadB(lock);threadB.start();} }
运行服务,输出结果如下:
2023-09-2816:42:19当前线程:Thread-0waitbegin2023-09-2816:42:22当前线程:Thread-1notifybegin2023-09-2816:42:22当前线程:Thread-1notifyend2023-09-2816:42:22当前线程:Thread-0waitend
从日志上可以得出,threadA线程先启动,然后进入阻塞状态,过了 3 秒之后,再启动threadB线程,运行结束之后,通知threadA线程可以获取对象锁,最后执行完毕。
整个线程之间的协调和通信,大体就是这样的。
假如我们把threadA线程数量增加到 5 个,再来看看运行效果。
publicclass MyThreadTest {publicstatic void main(String[]args)throws InterruptedException { Objectlock=new Object();// 创建5个wait线程for(inti=0;i<5;i++){ MyThreadA threadA=new MyThreadA(lock);threadA.start();}//过3秒再启动下一个线程Thread.sleep(3000);MyThreadB threadB=new MyThreadB(lock);threadB.start();} }
运行服务,输出结果如下:
2023-09-2817:02:05当前线程:Thread-0waitbegin2023-09-2817:02:05当前线程:Thread-4waitbegin2023-09-2817:02:05当前线程:Thread-3waitbegin2023-09-2817:02:05当前线程:Thread-2waitbegin2023-09-2817:02:05当前线程:Thread-1waitbegin2023-09-2817:02:08当前线程:Thread-5notifybegin2023-09-2817:02:08当前线程:Thread-5notifyend2023-09-2817:02:08当前线程:Thread-0waitend
从日志中,可以很清晰的看到,当多个线程处于等待状态时,调用notify()方法,只会唤醒其中一个等待的线程;同时服务无法关闭,因为剩下的 4 个线程一直处于阻塞状态。
假如我们把MyThreadB类中的lock.notify()方法改成lock.notifyAll()方法,再看看效果怎样。
publicclass MyThreadB extends Thread{ private Objectlock;publicMyThreadB(Objectlock){ this.lock=lock;}@Overridepublicvoid run(){ synchronized(lock){ System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" notify begin");// 唤醒所有等待的线程lock.notifyAll();System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" notify end");} } }
运行服务,输出结果如下:
2023-09-2817:18:13当前线程:Thread-0waitbegin2023-09-2817:18:13当前线程:Thread-4waitbegin2023-09-2817:18:13当前线程:Thread-3waitbegin2023-09-2817:18:13当前线程:Thread-2waitbegin2023-09-2817:18:13当前线程:Thread-1waitbegin2023-09-2817:18:16当前线程:Thread-5notifybegin2023-09-2817:18:16当前线程:Thread-5notifyend2023-09-2817:18:16当前线程:Thread-1waitend2023-09-2817:18:16当前线程:Thread-2waitend2023-09-2817:18:16当前线程:Thread-3waitend2023-09-2817:18:16当前线程:Thread-4waitend2023-09-2817:18:16当前线程:Thread-0waitend
从日志上可以很清晰的看到,3 秒后所有处于等待的线程都被唤醒,并且服务运行结束。
在多线程的编程中,任何时候都要关注锁,因为它对当前代码执行是否安全,发挥了重要的作用。
在上面我们提到,调用wait()方法,除了让线程进入阻塞,进入等待状态以外,还会释放锁。
我们可以看一个简单的示例就知道了。
publicclass MyThreadA1 extends Thread{ private Objectlock;publicMyThreadA1(Objectlock){ this.lock=lock;}@Overridepublicvoid run(){ synchronized(lock){ System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" wait begin");try {// 进入阻塞等待lock.wait();} catch(Exception e){ e.printStackTrace();} System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" wait end");} } }
publicclass MyThreadTest1 {publicstatic void main(String[]args)throws InterruptedException { Objectlock=new Object();// 创建两个调用wait的线程MyThreadA1 threadA1=new MyThreadA1(lock);threadA1.start();MyThreadA1 threadA2=new MyThreadA1(lock);threadA2.start();} }
运行服务,输出结果如下:
2023-09-2817:31:56当前线程:Thread-0waitbegin2023-09-2817:31:56当前线程:Thread-1waitbegin
从日志结果可以清晰的看出,两个线程中其中一个调用lock.wait()之后,进入了阻塞状态,同时把对象锁也释放掉了,另一个线程拿到锁并进入同步代码块内,所以看到两个线程都打印了wait begin。
在Thread类中也有一个sleep()方法可以让当前线程阻塞,但是它们之间是有区别的,sleep()方法不会让当前线程释放锁。
我们可以看一个简单的例子。
publicclass MyThreadA1 extends Thread{ private Objectlock;publicMyThreadA1(Objectlock){ this.lock=lock;}@Overridepublicvoid run(){ synchronized(lock){ System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" sleep begin");try {// 进入阻塞等待Thread.sleep(100);} catch(Exception e){ e.printStackTrace();} System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" sleep end");} } }
publicclass MyThreadTest1 {publicstatic void main(String[]args)throws InterruptedException { Objectlock=new Object();// 创建两个调用sleep的线程MyThreadA1 threadA1=new MyThreadA1(lock);threadA1.start();MyThreadA1 threadA2=new MyThreadA1(lock);threadA2.start();} }
运行服务,输出结果如下:
2023-09-2817:55:20当前线程:Thread-0sleepbegin2023-09-2817:55:21当前线程:Thread-0sleepend2023-09-2817:55:21当前线程:Thread-1sleepbegin2023-09-2817:55:21当前线程:Thread-1sleepend
从日志上看,线程没有交替执行,而是串性执行。
于此对应的还有notify()和notifyAll(), 调用notify()或者notifyAll()方法当前线程是不会释放锁的,只有当同步方法/同步代码块执行完毕,才会释放锁。
同样的,我们可以看一个简单的示例。
publicclass MyThreadA2 extends Thread{ private Objectlock;publicMyThreadA2(Objectlock){ this.lock=lock;}@Overridepublicvoid run(){ synchronized(lock){ System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" notify begin");// 唤醒其它等待线程lock.notify();System.out.println(DateUtil.format(newDate())+" 当前线程:"+Thread.currentThread().getName()+" notify end");} } }
publicclass MyThreadTest2 {publicstatic void main(String[]args)throws InterruptedException { Objectlock=new Object();// 创建两个调用notify()的线程MyThreadA2 threadA1=new MyThreadA2(lock);threadA1.start();MyThreadA2 threadA2=new MyThreadA2(lock);threadA2.start();} }
运行服务,输出结果如下:
2023-09-2818:11:36当前线程:Thread-0notifybegin2023-09-2818:11:36当前线程:Thread-0notifyend2023-09-2818:11:36当前线程:Thread-1notifybegin2023-09-2818:11:36当前线程:Thread-1notifyend
从日志结果可以清晰的看出,两个线程没有交替执行,而是串行执行。
虽然wait()、notify()、notifyAll()方法是在 Object 类中,理论上每个类都可以直接调用,但不是每个地方都可以随便调用,如果调用这三个方法,不在同步方法/同步代码块中,程序运行时会直接抛一次抛异常java.lang.IllegalMonitorStateException。
下面我们看一个简单的示例就知道了。
publicclass MyThreadTest3 {publicstatic void main(String[]args)throws Exception { Objectlock=new Object();lock.wait();} }
运行程序,直接抛异常。
Exceptioninthread"main"java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method)at java.lang.Object.wait(Object.java:502)at com.example.thread.e3.MyThreadTest3.main(MyThreadTest3.java:19)
换成notify()、notifyAll(),运行结果也是一样。
本文主要围绕线程之间的协调和通信相关技术进行一些知识总结,使用Object类中的wait()、notify()、notifyAll()方法,可以实现线程之间的协调和通信,但是它们只有在synchronized修饰的同步方法/同步代码块才会生效。如果不在同步方法/同步代码块调用,会抛java.lang.IllegalMonitorStateException异常。
文章内容难免有所遗漏,欢迎网友留言指出!
1、廖雪峰 - wait和notify介绍
2、五月的仓颉 - wait()和notify()/notifyAll()介绍