赞
踩
认识一下 Object 类中的两个和多线程有关的方法:wait 和 notify。
wait,当前线程进入 WAITING 状态,释放锁资源。
notify,唤醒等待中的线程,不释放锁资源。
实现一个容器报警功能,两个线程分别执行以下任务:
t1 为容器添加10个元素;
t2实时监控容器中元素的个数,当个数为5时,线程2给出提示并结束。
- /**
- * 一个普通的容器
- */
- 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 资源。
显然,这么做无法满足我们的要求。
- 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 报警,上述代码无论执行多少次都是最后输出报警信息,想想这是为什么?
- 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 是唤醒全部等待线程,但需要明确是是,由于wait-notify 的操作模式是基于锁对象的,所以即便是 notifyAll 也是非公平竞争锁资源,即哪个线程抢到锁就去执行同步代码。
我们声明了一个 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的使用场景,而且不受锁的限制,书写简便。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。