为什么有等待-通知机制?

首先,设想这样一种场景:一个线程的执行需要满足某些条件,当条件不满足时就通过一个循环不断尝试,直到条件满足。

这个场景下存在一个明显的缺点,就是线程不断地尝试获取所需的条件,这个循环的过程会白白浪费CPU资源,降低系统性能。

等待-通知机制是一种优化策略,其核心思想就是:当线程所需条件不满足时,就阻塞该线程,之后当条件满足时再通知线程,以此提高硬件资源的利用率

Java 中的实现

在 Java 中实现等待-通知机制,一种经典的做法是使用 synchronized + wait() + notify() / notifyAll()

我们可以通过一个经典的面试题来讲解这种用法:

请使用等待-通知机制实现两个线程交替打印 1~100

这个问题的解法有很多,我这里展示一下我自己的方法:

public class AlternatelyPrint {

    private int curNum = 0;

    synchronized void printNum(int n) {
        // 用 n - curNum 判断是否轮到当前打印数字
        while (n - curNum != 1) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + ":\t" + n);
        curNum += 1;
        notifyAll();
    }

    public static void main(String[] args) {
        AlternatelyPrint alternatelyPrint = new AlternatelyPrint();

        // OddThread 打印奇数
        new Thread(() -> {
            for (int i = 1; i <= 100; i += 2) {
                alternatelyPrint.printNum(i);
            }
        }, "OddThread").start();

        // EvenThread 打印偶数
        new Thread(() -> {
            for (int i = 2; i <= 100; i += 2) {
                alternatelyPrint.printNum(i);
            }
        }, "EvenThread").start();
    }

}

这段代码创建了两个线程 oddThreadevenThread 它们分别负责输出 1~100 之间的奇数和偶数。

AlternatelyPrint 中的 curNum 是共享变量,用于记录当前整个程序输出的位置,随着两个线程的交替输出,curNum 会随之递增

printNum() 为同步方法,每次只允许一个线程进入。在进入方法时线程首先通过 n - curNum 来判断当前是否轮到自己输出,如果不是则调用 wait() 将自己阻塞并且解锁释放资源,让其他线程来执行;如果是则正常执行方法,执行完成后调用 notifyAll 通知等待队列中正在等待的线程来执行。

其实,如果你熟悉操作系统的话应该知道,Java 中 synchronized 其实就是操作系统中管程的一种实现,synchronized 属于 Java 语言层面的管程实现。但是它的使用不灵活,而且效率较低,因此 JDK 中提供了更加灵活,更加高效的管程实现 LockCondition

Lock & Condition 的优势

Tips

1. 总是在 synchronized 内部调用 wait() + notify() + notifyAll()

阻塞与唤醒线程的前提一定是必须先持有互斥锁,有锁我们才有权利决定是否释放共享资源。其次还要注意,必须清楚当前的互斥锁的对象是谁,即 synchronized 锁的范围是什么,默认如果不加指定的话就是 this 对象,如果试图在 synchronized 锁定的范围外调用阻塞或唤醒方法,将会导致 java.lang.IllegalMonitorStateException

2. 总是在循环内使用 wait()

官方在 Object.wait() 的注释中给出了这样的建议。因为如果不适用循环可能会导致虚假唤醒的问题。notify()notifyAll() 只会告诉被阻塞的线程它所需要的条件曾经满足过,而被通知线程的执行时间点和通知的时间点基本上不会重合,所以当线程执行的时候,很可能条件已经不满足了(保不齐有其他线程插队)。当被阻塞线程被唤醒后,是从 wait() 方法之后开始继续运行的,如果此时已经被其他线程插队而导致条件不满足,将会导致程序执行出错。

3. 尽量使用 notifyAll()

notify() 会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程。因为 notify() 是随机唤醒一个线程的,因此可能会导致某个线程一直得不到唤醒的情况。建议当满足以下三个条件时使用 notify():

这篇文章是我学习王宝令老师讲授的《Java并发编程实战》的笔记和总结

极客时间 王宝令 《Java并发编程实战》

赞 赏
真诚赞赏 手有余香
用微信请DangHT喝杯咖啡?

微信支付

用支付宝请DangHT喝杯咖啡?

支付宝