面试官:为什么wait()方法要放在同步块中?

引言

这个东西是我今天看多线程通信的时候无意中想到的,为什么像wait()、notify()、notifyAll()之类的线程间通信需要放在同步块种,换言之为什么要用synchronized。jie如果wait()方法不在同步块中,会怎么样嘞:

@Test
public void test() {
    try {
        new Object().wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
复制代码

结果是:

经过一番谷歌,参照了各路大神的博客,终于找到了答案。

Lost Wake-Up Problem

首先我们来举个例子,一个消费者线程、一个生产者线程。生产者的任务为count+1,然后唤醒消费者;消费者的任务为count-1,等到count为0时陷入沉睡。

生产者伪代码:

count++;
notify();
复制代码

消费者伪代码:

while(count <= 0){
    wait();
    count--;
}

复制代码

熟悉多线程的朋友应该一眼就看出来了问题,如果生产者和消费者的步骤混杂在一起会发生什么。

首先我们先假设count = 0,这个时候消费者检查count的值,发现count <= 0的条件成立;就在这个时候,发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了,也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,所以这个通知就会被丢掉。紧接着,消费者就睡过去了……

图片来自为什么wait()会这样

扫描二维码关注公众号,回复: 9813393 查看本文章

这就是所谓的Lost Wake-Up Problem

如何解决

现在我们应该能发现问题的根源在于,消费者在检查count到调用wait()之间,count就可能被改掉了。 那我们如何解决呢?
让消费者和生产者竞争一把锁,竞争到了的,才能够修改count的值。

于是乎生产者代码:

lock();
count++;
notify();
unlock();
复制代码

消费者代码:

lock();
while(count <= 0){
    wait();
    count--;
}
unlock();
复制代码

现在我们来看看,这样子真的解决了吗?

答案是毫无卵用,依旧会出现lost wake up问题,而且和无锁的表现是一样的。

因为wait()是释放锁然后等待获取锁,当然要先获得锁才行,但在这边连锁都莫得。

终极答案

所以,我们可以总结到,为了避免出现这种 lost wake up 问题,在这种模型之下,总应该将我们的代码放进去的同步块中。

Java强制我们的 wait()/notify() 调用必须要在一个同步块中,就是不想让我们在不经意间出现这种 lost wake up 问题。

不仅仅是这两个方法,包括 java.util.concurrent.locks.Condition 的 await()/signal() 也必须要在同步块中。

正解:

private Object obj = new Object();
private Object anotherObj = new Object();

@Test
public void produce() {
    synchronized (obj) {
        try {
            //同步块要对当前线程负责,而不是anotherObj.notify();
            obj.notify();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码

wait和notify的用法

wait()、notify()和notifyAll()

  • wait()、notify() 和 notifyAll()方法是本地方法,并且为 final 方法,无法被重写。

  • 调用某个对象的 wait() 方法能让当前线程阻塞,并且当前线程必须拥有此对象的 monitor(即锁,或者叫管程)。

  • 调用某个对象的 notify() 方法能够唤醒一个正在等待这个对象的 monitor 的线程,如果有多个线程都在等待这个对象的 monitor,则只能唤醒其中一个线程。

  • 调用 notifyAll() 方法能够唤醒所有正在等待这个对象的monitor的线程。

具体应用


/**
 * wait() && notify()方法
 * 这两个方法是在Object中定义的,用于协调线程同步,比 join 更加灵活
 */
public class NotifyDemo {
    public static void main(String[] args) {
        //写两个线程 1.图片下载
        Object obj=new Object();
        Thread download=new Thread(){
            public void run() {
                System.out.println("开始下载图片");
                for (int i = 0; i < 101; i+=10) {
                    System.out.println("down"+i+"%");
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("图片下载成功");
                synchronized (obj) {
                    obj.notify();//唤起
                }
                System.out.println("开始下载附件");
                for (int i = 0; i < 101; i+=10) {
                    System.out.println("附件下载"+i+"%");

                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
                System.out.println("附件下载成功");
            }
        };
        //2.图片展示
        Thread show=new Thread(){
            public void run(){
                synchronized (obj) {
                    try {
                        obj.wait();//阻塞当前
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("show:开始展示图片");
                    System.out.println("图片展示完毕");
                }

            }
        };
        download.start();
        show.start();
    }
}
复制代码

猜你喜欢

转载自juejin.im/post/5e6a4d8a6fb9a07cd80f36d1