简介
本文讲解Java中wait()
、notify()
,通过一个标准的使用实例,来讨论下这两个方法的作用和使用时注意点,这两个方法被提取到顶级父类Object
对象中,地位等同于toString()
方法,所以本文带你从零开始搞懂它们的用法,在文章最后,准备了一个《捡肥皂》的故事,就算你没写过代码,读了此故事也能明白wait()
、notify()
系列方法在程序中的作用了,也算是在1024程序员节送给大家的彩蛋了。
一.wait()和notify()系列方法含义
wait()
方法是让当前线程等待的,即让线程释放了对共享对象的锁。
wait(long timeout)
方法可以指定一个超时时间,过了这个时间如果没有被notify()
唤醒,则函数还是会返回。如果传递一个负数timeout
会抛出IllegalArgumentException
异常。
notify()
方法会让调用了wait()
系列方法的一个线程释放锁,并通知其它正在等待(调用了wait()方法)的线程得到锁。
notifyAll()
方法会唤醒所有在共享变量上由于调用wait系列方法而被挂起的线程。
注意:
- 调用
wait()
、notify()
方法时,当前线程必须要成功获得锁(必须写在同步代码块锁中),否则将抛出异常。 - 只对当前单个共享变量生效,多个共享变量需要多次调用
wait()
方法。 - 如果线程A调用
wait()
方法后处于堵塞状态时,其他线程中断(在其他线程调用A.interrupt()方法)A线程,则会抛出InterruptExcption
异常而返回并终止。
理论内容就这些,下面将上述内容用实例展示给大家,并一步一步带着大家分析和实现这两个方法,多线程中这两个方法会让程序跳跃执行,所以一定要搞清楚代码的执行流程。
二.标准代码示例
1.代码实现内容流程描述:
- 创建两个线程
Thread0
和Thread1
。 - 让
Thread0
执行wait()
方法。 - 此时
Thread1
得到锁,再让Thread1
执行notify()
方法释放锁。 - 此时
Thread0
得到锁,Thread0
会自动从wait()
方法之后的代码,继续执行。
通过上述流程,我们就可以清楚的看到,wait()
和notify()
各自是怎么工作的了,也可以知道两者是怎么配合的了。
2.代码实现:
public class ThreadWaitAndNotify {
// 创建一个将被两个线程同时访问的共享对象
public static Object object = new Object();
// Thread0线程,执行wait()方法
static class Thread0 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "初次获得对象锁,执行中,调用共享对象的wait()方法...");
try {
// 共享对象wait方法,会让线程释放锁。
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "再次获得对象锁,执行结束");
}
}
}
// Thread1线程,执行notify()方法
static class Thread1 extends Thread {
@Override
public void run() {
synchronized (object) {
// 线程共享对象,通过notify()方法,释放锁并通知其他线程可以得到锁
object.notify();
System.out.println(Thread.currentThread().getName() + "获得对象锁,执行中,调用了共享对象的notify()方法");
}
}
}
// 主线程
public static void main(String[] args) {
Thread0 thread0 = new Thread0();
Thread1 thread1 = new Thread1();
thread0.start();
try {
// 保证线程Thread0中的wait()方法优先执行,再执线程Thread1的notify()方法
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.start();
}
3.运行结果
Thread-0初次获得对象锁,执行中,调用共享对象的wait()方法...
Thread-1获得对象锁,执行中,调用了共享对象的notify()方法
Thread-0再次获得对象锁,执行结束
4.运行流程详解:
从执行的结果中,要明白线程的执行顺序:
Thread0
调用了wait()
方法后,会释放掉对象锁并暂停执行后续代码,即从wait()
方法之后到run()
方法结束的代码,都将立即暂停执行,这就是wait()
方法在线程中的作用。- CPU会将对象锁分配给一直等候的
Thread1
线程,Thread1
执行了notify()
方法后,会通知其他正在等待线程(Thread0
)得到锁,但会继续执行完自己锁内的代码之后,才会交出锁的控制权。 - 因为本例只有两个线程,所以系统会在
Thread1
交出对象锁控制权后(Synchronized
代码块中代码全部执行完后),把锁的控制权给Thread0
(若还有其他线程,谁得到锁是随机的,完全看CPU心情),Thread0
会接着wait()
之后的代码,继续执行到Synchronized
代码块结束,将对象锁的控制权交还给CPU。
三.用生活故事讲懂线程的等待唤醒
Java线程的等待唤醒机制,是通过wait()
方法和notify()
方法实现的,为了更好的理解,我再来举一个通俗易懂接地气的例子吧,帮不懂代码的人也能明白这两个方法的作用。
例:捡肥皂的故事
假设有两个程序员去洗澡,只带了一块肥皂,两个人怎么使用一块肥皂洗澡呢?会发生3个场景:
1.老王和老李(专家程序员):
老王和老李随机一人拿到肥皂,比如老王先拿到肥皂,然后使用肥皂,然后把肥皂让出去,自己等会再用。老李拿到了肥皂,然后使用了一会,再通知老王说:“自己不用了”,老王听到话以后,捡起肥皂从上次用的地方接着用。二者洗澡,你来我往共享一块肥皂,非常和谐。
程序语言描述:
老王随机先得到锁,然后用了一会后,调用了wait()方法,把锁交了出去,自己等待。老李拿到锁,使用后,再通过notify()通知老王,然后等老李用完以后,老王再次拿到锁,继续执行…这种方式是线程安全的,而且还能合理的分配资源的使用,这就是等待唤醒的好处。
2.王哥和李哥(普通程序员):
王哥和李哥随机一人拿到肥皂,比如王哥先拿到,然后王哥就一直霸占着,直到自己洗完了,才把肥皂给李哥。期间李哥洗澡只能干搓,根本没机会接触肥皂。我想李哥肯定觉得王哥很自私,不懂得礼让,李哥的体验不是很好。
程序语言描述:王哥和李哥就是两个线程,王哥在拿到锁以后,就一直使用,直到同步代码块中的内容完全执行完成。再把锁交给李哥使用。这种方式每次都是一个线程执行完,另一个才会执行,是线程安全的。
3.小王和小李(新手程序员):
小王和小李一开始洗澡就争抢肥皂,当肥皂在小王手上时,小王还在使用中,小李就扑上来了,于是出现了两人一起摩擦一块肥皂的场景!这种画面既不优雅,也不安全。
程序语言描述:
如果两个线程,访问同一个资源的时候,不对其进行加锁控制,就会出现混乱的场景,这就是线程不安全。两个线程可能会同时操作同一共享变量,从而使这个共享变量失控,最终结果紊乱。
我们可以看出老王和老李的配合是最默契的,对肥皂这个资源的利用率是最高的。张哥和李哥虽然也能分配资源,但对资源的利用效率不高,始终有一个人,要等待另一个人完全不用了,才能获得。小张和小李是完全不懂配合的,他们都想争抢肥皂,最终是谁都没有用好,场面陷入了混乱,画面不忍直视。。。
(本文发表于2020年1024程序员节,以此故事,向各位程序员致敬。1024+996=2020)
总结
通过上面故事中的三个例子,我想你应该明白了线程的等待唤醒的使用场景以及好处了吧,你理解了这些通俗的例子,才能在程序中将其思想发挥出来,从而使用wait()
和notify()
方法,构建出一个合理的程序流程,优雅的实现线程之间变量的共享使用。喜欢本文请收藏点赞,祝大家1024节日快乐。