为什么object.wait()、object.notify()一定要放在synchronized代码块内?

    相信大多数人对object.wait()和object.notify()都非常熟悉,最经典的生产者-消费者模型就可以基于wait-notify机制来实现的,那么在编写代码的时候发现,JDK要求对object.wait()和object().notify方法必须在synchronized代码块内部使用,否则运行时会抛出IllegalMonitorStateException异常。那么为什么JDK要对此做限制呢?

    要想知道为什么要加此限制,就得知道不加此限制会发生什么非预期的问题。如果不加这个限制,一个简单的生产者-消费者模型的实现如下:当count为0的时候,生产者进行生产操作,并将count+1,然后调用notify()方法通知;当count为0时,消费者会调用wait()方法进行等待。(至于为什么消费者中要用while()方法,我们在后文介绍)

private int count = 0;
private Object obj;

public void producer(){
    if (count == 0){
        //省略生产逻辑
	count++;
        obj.notify();
    }
}

public void consumer(){
    while (count == 0){
	obj.wait();	
    }
    //省略消费逻辑
}

    乍一看,通过上述代码实现了生产者-消费者的功能,但是仔细一想,存在问题。假如此时线程T1在执行producer的逻辑,线程T2在执行consumer的逻辑,如果代码的执行顺序变成下面这样,就会有问题:

        1.线程T2执行while (count == 0),表达式成立,进入while循环;

        2.线程T1执行if (count == 0),表达式成立,进入if消息体;

        3.线程T1执行if消息体内容,最终调用obj.notify()(注意,此时线程T2未执行obj.wait(),notify()不会唤醒任何线程);

        4.线程T2执行obj.wait()进行等待;

    这样执行完之后,count的值为1,生产者不会再进行生产操作(也就不会调用obj.notify(),而此时消费者线程T2处于等待状态(需要obj.notify()来唤醒),消费者线程就永远地死等下去了,这就是多线程编程中臭名昭著的Lost Wake-Up Problem问题。

为什么使用synchronized同步代码块能解决这个问题呢?

    仔细分析上面的问题,原因很简单,就是对count变量的读写存在竞态条件,举个例子,consumer()方法原本的用意是在执行obj.wait()的时候,count的值必须为0,也就是obj.wait()和count为0是绑定的,但是此时如果有另外一个线程在执行producer()方法,可能就会在执行while (count == 0)到obj.wait()之间对count的值进行修改,从而出现非预期的情况(即在执行obj.wait()方法的时候,count的值不是0)。

    而使用synchronized同步代码块后,代码变成这个样子:

private int count = 0;
private Object obj;

public void producer(){
    synchonized(obj){
        if (count == 0){
            //省略生产逻辑
	    count++;
            obj.notify();
        }
    }
}

public void consumer(){
    synchonized(obj){
        while (count == 0){
	    obj.wait();	
        }
        //省略消费逻辑
    }
}

    此时,由于只有线程拿到obj对象的锁才能进入同步代码块,所以能够保证生产者和消费者只有一个线程在执行,也就保证了在执行while (count == 0)到obj.wait()之间count的值不会发生改变,也就是上面1、4步骤之间,不可能会有2、3步骤了。

为什么要使用while(count == 0)而不是if(count ==0)?

    要想知道为什么使用while(count == 0),可以先看看使用if(count ==0)会有什么问题。还是基于上面的代码实例,当前两个线程的执行情况是,线程T1执行obj.notify()方法,线程T2执行obj.wait()方法。如果此时有一个线程T3也作为消费者开始执行consumer()方法,可能会出现这种情况:

    1.线程T1执行obj.notify();

    2.线程T2被唤醒(注意:唤醒操作只是将线程从管程中的等待队列中拿取来放到管程的入口队列中去竞争锁,而不是直接得到锁),尝试去竞争obj对象锁;

    3.线程T3执行consumer()方法,竞争到锁,并进行消费,将count-1,即count又变为0,然后释放锁;

    4.线程T2获取到锁,执行消费逻辑(因为为if(count == 0),所以直接往下执行消费逻辑了);

很明显,此时count已经被线程T3消费掉了,count的值又变回0了,线程T2去执行消费逻辑是存在问题的,这就是虚假唤醒的问题。但是如果将if(count == 0)改为while(count == 0)就不会有问题了,因为线程T2拿到锁之后还会去判断一下count的值是不是0,非0的情况下才会去执行消费的逻辑。

    所以,使用等待-通知机制时有一个经典范式:

while(条件不满足){
    wait();
}

 

你可能感兴趣的:(#Java基础,并发编程)