为什么wait/notify必须要强制要求放在synchronized中

1 为什么wait/notify必须要强制要求放在synchronized中

在日常开发,我们都知道wait/notify有着非常固定的一套模板,就是下面的样子,synchronized同步块包裹着Object.wait()方法,如果不通过同步块包住的话JVM会抛出IllegalMonitorStateException异常。

synchronized(lock) {
	while(!condition){
        lock.wait();
    }
}

那么为什么要限制这么写呢?

2 如果Object.wait()/notify不需要同步

假设我们自己实现了一个BlockingQueue的代码。
如果Object.wait()/notify不需要同步,那么我们的代码会形如下面这样。

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // 往队列里添加的时候notify,因为可能有人在等着take
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // 用while,防止spurious wakeups(虚假唤醒)
            wait(); // 当buffer是空的时候就等着别人give
        return buffer.remove();
    }
}

如果上面的代码可以执行的话,多线程情况下会出现一种情况:

  1. 当前buffer是空的,这时来了一个take的请求,尚未执行到wait语句
  2. 同时又来了一个give请求,完整执行完了整个give方法并且发送了notify
  3. 此时take方法才走到wait,因为它错过了上一个notify,所以会在明明buffer不空的情况下挂起线程,take方法挂起。假如再没有人调用过give方法了,在业务上的表现就会是这个take线程永远也取不到buffer中的内容
    执行顺序

3 为什么要在JVM层面抛异常

因为你只要用notify,那就是为了在多线程环境下同步,notify/wait机制本身就是为了多线程的同步而存在的,那就只能配套synchronized,所以为了防止上面情况的发生,就直接强制抛异常来限制开发的代码模式了。

4 如果没有wait/notify

试想一种场景,如果没有wait/notify的挂起唤醒机制,该如何实现BlockingQueue

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
    }

    public String take() throws InterruptedException {
        while(buffer.isEmpty) {
            sleep(10);
        }
        return buffer.remove();
    }
}

如果没有wait/notify,那么在take的时候只能通过while循环不停轮询判断buffer是否为空来实时获取buffer的最新状态,那么势必会造成两种情况:

  1. sleep时间过短,那么线程将一直不行循环抢占CPU,造成CPU打满
  2. sleep时间过长,那么将会影响take的时效性

综上,针对于BlockingQueue这样的场景,同步块 + wait/notify 或者 lock + signal/await 就是标配

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
哲学家进餐问题是一个经典的同步问题,它描述了五位哲学家围坐在一张圆桌前,每个哲学家面前有一碗米饭和一只筷子,相邻的两个哲学家共享一只筷子。每个哲学家必须交替地进行思考和进餐,但是进餐需要使用两只筷子,因此问题在于如何避免死锁。 使用wait/notify机制可以解决哲学家进餐问题,具体实现如下: ```java public class Philosopher implements Runnable { private Object leftChopstick; private Object rightChopstick; public Philosopher(Object leftChopstick, Object rightChopstick) { this.leftChopstick = leftChopstick; this.rightChopstick = rightChopstick; } public void run() { try { while (true) { // 思考 System.out.println(Thread.currentThread().getName() + " is thinking"); Thread.sleep((int) (Math.random() * 1000)); synchronized (leftChopstick) { System.out.println(Thread.currentThread().getName() + " picked up left chopstick"); synchronized (rightChopstick) { // 进餐 System.out.println(Thread.currentThread().getName() + " is eating"); Thread.sleep((int) (Math.random() * 1000)); } System.out.println(Thread.currentThread().getName() + " put down right chopstick"); } System.out.println(Thread.currentThread().getName() + " put down left chopstick"); // 重复循环 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } public static void main(String[] args) { Object[] chopsticks = new Object[5]; for (int i = 0; i < chopsticks.length; i++) { chopsticks[i] = new Object(); } for (int i = 0; i < 5; i++) { Object leftChopstick = chopsticks[i]; Object rightChopstick = chopsticks[(i + 1) % 5]; Philosopher philosopher = new Philosopher(leftChopstick, rightChopstick); new Thread(philosopher, "Philosopher " + (i + 1)).start(); } } } ``` 在上述代码,每个哲学家线程都会进行思考和进餐的循环,当哲学家想要进餐时,它会先尝试获取左边的筷子,如果获取成功,则再尝试获取右边的筷子,如果获取成功,则进餐,否则会释放左边的筷子。在获取筷子时,使用了synchronized同步块,确保了同一时刻只有一个线程可以使用同一只筷子。此外,当线程进入wait状态时,会自动释放它所持有的所有锁,这也有助于避免死锁的发生。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值