引言
认识一下 Object 类中的两个和多线程有关的方法:wait 和 notify。
wait,当前线程进入 WAITING 状态,释放锁资源。
notify,唤醒等待中的线程,不释放锁资源。
一、使用 wait-notify 实现一个监控程序
实现一个容器报警功能,两个线程分别执行以下任务:
t1 为容器添加10个元素;
t2实时监控容器中元素的个数,当个数为5时,线程2给出提示并结束。
1.1 简单的 while-true 版本
/*** 一个普通的容器*/
class Container {private volatile List<Object> values = new ArrayList<>();public void add(Object value) {values.add(value);}public Integer sise() {return values.size();}
}
public class T_Alert01 {public static void main(String[] args) {Container container = new Container();new Thread(() -> {while (true) {if (container.sise() == 5) {System.out.println("alert 5!");break;}}}, "T2").start();new Thread(() -> {for (int i = 0; i < 10; i++) {container.add(new Object());System.out.println("已添加:" + container.sise());}}, "T1").start();}
}
程序解析:监控线程 T2 通过 while-true 监控容器内的元素数量,但当 size == 5 时,还未来得及报警,T1 就又添加了多个元素;而且 while-true 会浪费很多CPU 资源。
显然,这么做无法满足我们的要求。
1.2 wait-notify 初版
public class T_Alert02 {public static void main(String[] args) throws InterruptedException {Container container = new Container();final Object lock = new Object();new Thread(() -> {synchronized (lock) {if (container.sise() != 5) {try {lock.wait();} catch (InterruptedException e) {}}System.out.println("alert 5!");}}, "T2").start();TimeUnit.SECONDS.sleep(1);new Thread(() -> {synchronized (lock) {for (int i = 0; i < 10; i++) {container.add(new Object());System.out.println("已添加:" + container.sise());if (container.sise() == 5) {lock.notify();}}}}, "T1").start();}
}
程序解析:T2监控线程先去判断容器大小,如果未达到报警标准,则等待在 lock 对象上,WAITING 是一种挂起状态,不消耗CPU资源。
T1 线程在达到 5 个时,触发一个 notify方法,唤醒其他等待中的线程。然而,仅仅是 notify 还不足以做到实时唤醒 T2 报警,上述代码无论执行多少次都是最后输出报警信息,想想这是为什么?
1.3 wait-notify 完整版
public class T_Alert03 {public static void main(String[] args) throws InterruptedException {Container container = new Container();final Object lock = new Object();new Thread(() -> {synchronized (lock) {if (container.sise() != 5) {try {lock.wait();} catch (InterruptedException e) {}}System.out.println("alert 5!");lock.notify();}}, "T2").start();TimeUnit.SECONDS.sleep(1);new Thread(() -> {synchronized (lock) {for (int i = 0; i < 10; i++) {container.add(new Object());System.out.println("已添加:" + container.sise());if (container.sise() == 5) {lock.notify();try {lock.wait();} catch (InterruptedException e) {}}}}}, "T1").start();}
}
程序解析:该版本的 wait-notify 可以满足实际题目要求,达到实时触发警报,在 T1 notify 之后立刻调用 wait 进入状态;另一边T2在被唤醒并输出报警信息后,也需要再次调用 notify 唤醒 其他等待线程继续执行任务。
二、notify 和 notifyAll
如果有多个线程等待同一个对象锁,那么 notify 方法会随机唤醒一个线程,它无法做到精准唤醒。notifyAll 是唤醒全部等待线程,但需要明确是是,由于wait-notify 的操作模式是基于锁对象的,所以即便是 notifyAll 也是非公平竞争锁资源,即哪个线程抢到锁就去执行同步代码。
三、wait-notify 的原理
我们声明了一个 Object 对象,直接调用 wait 方法会怎样呢?
public class T_Wait_Notify {public static void main(String[] args) throws InterruptedException {final Object lock = new Object();lock.wait();}
}
监视器状态异常。这是因为 wait-notify 必须基于“锁对象”,而这个锁对象可不是普通的一个什么对象都可以。在了解了 synchronized 关键字的实现原理后,我们知道,JVM 为每个对象都关联了一个 monitor 对象,进入同步代码块和结束同步代码块就对应着 monitor enter 和 monitor exit 两条指令,也就是说,如果不使用 synchronized,就不存在所谓的 wait 和 notify。所以正确的写法一定是:
synchronized (lock) {lock.wait();
}
在 wait 方法的 Java doc 中这样说明,wait 方法会令当前线程将自己放入锁对象的 wait set 中,并且放弃此对象上所有同步的同步声明。
当唤醒时,当前线程必须持有该对象的监视器,才能继续执行。
总之,wait 和 notify 是和 对象的 monitor 紧密相关的,而 monitor 又是 synchronized 重量级锁模式的实现原理,所以理解wait 和 notify的 同时 也需要深入理解 synchronized 关键字。
扩展:闭锁实现的监控报警
闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。
闭锁相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,不允许任何线程通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。
下面的程序是通过 CountDownLatch 闭锁来实现的一个监控容器的版本,虽然可以达到要求,但很遗憾,程序中必须通过 sleep 方法让线程跑的“没那么快”,否则,去掉 sleep 的话依然会出现 while-true 的执行结果,即警报没那么实时了:
public class T_Alert04CountDownLatch {public static void main(String[] args) {Container container = new Container();CountDownLatch latch = new CountDownLatch(1);new Thread(() -> {if (container.sise() != 5) {try {latch.await();} catch (InterruptedException e) {}}System.out.println("alert 5!");}, "T2").start();new Thread(() -> {for (int i = 0; i < 10; i++) {container.add(new Object());System.out.println("已添加" + container.sise());if (container.sise() == 5) {latch.countDown();}try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {}}}, "T1").start();}
}
总结
使用 wait-notify 切换线程状态是一种细粒度操作,开发者需要非常了解他们的执行逻辑以及线程的生命周期。
wait 和 notify使用时必须将对象锁定,否则无法使用。
线程在使用对象的wait方法后会进入等待状态,notify() 和 notifyAll() 可以唤醒其他线程,注意 notify 是随机唤醒一个线程。
wait-notify的操作是相对复杂的,虽然强大,但是在处理复杂的业务逻辑中书写较麻烦,相当于多线程中的汇编语言。
使用CountDownLatch可以有效的替代wait和notify的使用场景,而且不受锁的限制,书写简便。