notify和notifyAll的一段代码分析

【本文已经根据网友的意见,进行了修改,考虑到网友会看不懂评论内容,我将本文的第一版发布到了博客中,点击这里 踏雁寻花 可以阅读。 】

根据网友的意见,修改版如下:

当你Google”notify()和notifyAll()的区别”时,会有大片的结果弹出来,(这里先把jdk的javadoc文档那一段撇开不说),所有这些搜索结果归结为等待的线程被唤醒的数量:notify()是唤醒一个, 而notifyall()是唤醒全部.那他们的真正区别是什么呢?

让我们来看看个生产者/消费者的案例(假设生产者/消费者这个类中有两个方法put和get),它是有问题的(因为这里用到了notify方法),是的,这段代码也许会执行,甚至大部分情况下能够正常运行,但是它也是有可能会出发生死锁,我们来看一看原因:

public synchronized void put(Object o) {
    while ( buf.size() == MAX_SIZE) {
         wait(); // 如果buffer为full,就会执行wait方法等待(为了简单,我们省略try/catch语句块)
    }
    buf.add(o);
    notify(); // 通知所有正在等待对象锁的Producer和Consumer(译者注:包括被阻挡在方法外的Producer和Consumer)
}

// Y:这里是C2试图获取锁的地方(原作者将这个方法放到了get方法里面,此处,我把它放在了方法的外面)   
public synchronized Object get() {
    while ( buf.size() == 0) {
         wait(); // 如果buffer为Null,就会执行wait方法(为了简单,同样省略try/catch语句块)
          // X: 这里是C1试图重新获得锁的地方(看下面代码)
    }
    Object o = buf.remove(0);
    notify(); // 通知所有正在等待对象锁的Producer和Consumer(译者注:包括被阻挡在方法外的Producer和Consumer)
    return o;
}

首先

我们为什么在wait方法外面加上while循环?
我们需要使用while循环来实现下面的情景,
情景分析:
消费者1(C1)进入同步块中,此时buf是空的,所以C1被放入wait 队列中(因为执行了wait方法,译者注:此时C2是恰好到方法处,而不是因为有线程在方法中运行才被阻挡在方法外的),当消费者2(C2)正要进入同步方法的时候(此时在Y的上面),生产者P1将一个对象放入到buf中,随后又调用notify方法。此时wait 队列中唯一的线程是C1(译者注:C2不在waiting 队列中,也不在blocked队列中),所以C1被唤醒,C1被唤醒之后又开始试图重新获得对象锁,此时C1还在X的上面。
现在的情况是,C1和C2都在试图去获取同步锁,这两个线程只能有一个被选择进入方法,另一个则会被堵塞(不是waiting,而是blocked 。译者注:虽然C1已经在方法中,不过还是会和C2竞争锁,如果C2获得锁,则C2进入方法执行接下来的操作,而C1还是继续等待锁(处于blocked状态);如果C1获得锁,则C1往下执行,而C2还是会被挡在方法外面(处于blocked状态))。假如C2先获得了对象锁,C1仍然被阻挡着(此时C1还试图在X处获得锁),C2完成了方法,并释放了锁。现在C1获得了锁。假设这里没有while循环,那么C1就会往下执行,从buf中删除一个对象,但是此时buf中已经没有对象了,因为刚刚C2已经取走了一个对象,如果此时C1执行buf.remove(0),则会报IndexArrayOutOfBoundsException异常。为了防止这样的异常发生,我们在上面用到了while循环,在往下执行之前,判断此时buf的大小是否为0,如果不是0,则往下执行,如果是0,则继续wait()。

那么我们这里引出问题:为什么需要notifyAll?

在上面生产者-消费者这个例子中,看起来我们用notify也能够侥幸成功,因为等待循环的哨兵对于消费者和生产者来说是互斥的。我们不能同时在put方法和get方法都有一个线程wait,如果这种情况允许的话,那么下面的事情就会发生:

buf.size() == 0 AND buf.size() == MAX_SIZE (假设MAX_SIZE不为0)

然而,这样并不好,我们需要使用notifyAll。让我们来看一看原因:

假设 buffer=1(为了更加容易理解),按照下面的步骤执行将会发生死锁。要注意的是:notify可以唤醒任何一个线程,不过JVM不能确定是哪个线程被唤醒,所以,任何一个线程都有被唤醒的可能。另外要注意的是,当多个线程被阻塞在方法外的时候(在试图获得锁),获得锁的顺序也是不确定的。要记住,在任何时候,方法中只能有一个线程存在-在类中任何同步的方法只允许一个线程执行(这个线程要持有对象锁才可以执行)。如果下面的执行顺序发生了的话,就会导致死锁:

第一步:P1放入一个对象到buffer中;
第二步:P2试图put一个对象,此时buf中已经有一个了,所以wait
第三步:P3试图put一个对象,仍然wait
第四步:

  • C1试图从buf中获得一个对象;
  • C2试图从buf中获得一个对象,但是挡在了get方法外面
  • C3试图从buf中获得一个对象,同样挡在了get方法外面

第五步:

  • C1执行完get方法,执行notify,退出方法
  • notify唤醒了P2,
  • 但是C2在P2唤醒之前先进入了get方法,所以P2必须再次获得锁,P2被挡在了put方法的外面,
  • C2循环检查buf大小,在buf中没有对象,所以只能wait;
  • C3在C2之后,P2之前进入了方法,由于buf中没有对象,所以也wait;

第六步:

  • 现在,有P3,C2,C3在waiting;
  • 最后P2获得了锁,在buf中放入了一个对象,执行notify,退出put方法;

第七步:

  • notify唤醒P3;
  • P3检查循环条件,在buf中已经有了一个对象,所以wait;
  • 现在没有线程能够notify了,三个线程就会处于死锁状态。

下面是译者分析:

 那么如果使用notifyAll方法唤醒线程,又会怎样呢?

在执行第五步时,即C1执行完get方法后,又执行了notifyAll方法,此时,notifyAll方法会唤醒所有正在等待该锁的线程,那么所有的线程都会处于运行前的准备状态(此时不是wait状态),此时,即使C2在P2(此时P2已经被唤醒,P3也被唤醒,处于准备状态,而不是wait状态)之前先进入了get方法,C2循环检查buf大小,在buf中没有对象,所以进入wait状态;C3在C2之后,P2之前进入方法,由于buf中没有对象,所以也wait;(这里重新分析了一下步骤五发生的情景)

第六步:现在,有C2,C3在waiting,P3在第五步已经被唤醒了,处于准备状态,此时,如果P2获得锁,在buf中放入一个对象,执行notifyAll,又将C2、C3唤醒了;

第七步:此时,P3检查循环条件,在buf中已经有了一个对象,所以wait,不过此时并不会发生死锁,因为C2和C3还会继续执行。
 
总结:notify方法很容易引起死锁,除非你根据自己的程序设计,确定不会发生死锁,notifyAll方法则是线程的安全唤醒方法。

附:
notify和notifyAll的区别:
notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。
void notify(): 唤醒一个正在等待该对象的线程。
void notifyAll(): 唤醒所有正在等待该对象的线程。
两者的最大区别在于:

notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
notify他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁,此时如果该对象没有再次使用notify语句,即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。

以前大伙看到这两个区别的时候可能感觉到很懵,相信现在应该有些明白了吧,如果还没有搞清楚可以看一下我这里案例的分析:notify发生死锁的情景

 

根据网友的评论补充:

解释下前三步:
synchronized修饰的方法,同一时刻只能允许一个线程进入,所以第一步执行之后,P1执行notify,跳出方法,然后,P2可以进入synchronized修饰的put方法,不过这一次是wait,P2线程会释放对象锁,此时P3就可以进入put方法,当然了,还是wait(),释放了对象锁。
第四步:
get方法也是用synchronized修饰的,所以同一时刻,只能有一个线程进入此方法,C1进入之后,试图取走一个对象,但此时还没有取走,此时,C2准备进入get方法,不过因为这个方法是用synchronized修饰的,所以C2被挡在了方法的外面,同理,C3也被挡在了方法的外面。(注意:此时C1还没有取走对象)。

第五步:
当C1取走对象后,在执行notify方法之前,P2,P3会继续wait,继续等着notify的通知。
C1执行notify方法,通知了P2,此时P2可以获得对象锁了(这里的意思是说:P2可以去抢对象锁了,但是能不能抢得到就看它的造化了)。(注意:此时的C2以及C3还在外面等着,不过它们不是因为执行了wait而等,所以它们不需要等notify的通知,只要有对象锁,它们两个就可以争抢,获得了对象锁的就可以进入方法,所以,虽然notify通知了P2,但是C2和P2同属竞争关系,所以C2是可以在P2之前获得对象锁的)。

 

网友补充:

C2,C3在C1进入get方法后会被jvm放入对象的锁池中,而P2,P3是被放入对象的等待池中,等待池的线程只有通过notify、notifyAll或者interrupt才能进入锁池中,而锁池的线程只有拿到锁标识后才进入runnable状态等待cpu时间片。

原文链接: stackoverflow 翻译: ImportNew.com - 踏雁寻花
译文链接: http://www.importnew.com/10173.html
[ 转载请保留原文出处、译者和译文链接。]



相关文章

发表评论

Comment form

(*) 表示必填项

13 条评论

  1. 说道:

    这个案例好,mark下

    Thumb up 0 Thumb down 0

  2. ericzhang 说道:

    “但是C2在P2唤醒之前先进入了get方法,所以P2必须再次获得锁,P2被挡在了put方法的外面”

    这个怎么理解,C1 notify P2,不是把P2从wait状态唤醒并给了锁了吗?C2怎么可以抢在前面的,弱弱请教一下

    Thumb up 0 Thumb down 0

    • 宋 涛 说道:

      解释下前三步:
      synchronized修饰的方法,只能允许一个线程进入,所以第一步执行之后,P1执行notify,跳出方法,然后,P2可以进入synchronized修饰的put方法,不过这一次是wait,P2线程会释放对象锁,此时P3就可以进入put方法,当然了,还是wait(),释放了对象锁。

      第四步:
      get方法也是用synchronized修饰的,所以同一时刻,只能有一个线程进入此方法,C1进入之后,试图取走一个对象,但此时还没有取走,此时,C2准备进入get方法,不过因为这个方法是用synchronized修饰的,所以C2被挡在了方法的外面,同理,C3也被挡在了方法的外面。(注意:此时C1还没有取走对象)。

      第五步:
      当C1取走对象后,在执行notify方法之前,P2,P3会继续wait,继续等着notify的通知。
      C1执行notify方法,通知了P2,此时P2可以获得对象锁了(这里的意思是说:P2可以去抢对象锁了,但是能不能抢得到就看它的造化了)。(注意:此时的C2以及C3还在外面等着,不过它们不是因为执行了wait而等,所以它们不需要等notify的通知,只要有对象锁,它们两个就可以争抢,获得了对象锁的就可以进入方法,所以,虽然notify通知了P2,但是C2和P2同属竞争关系,所以C2是可以在P2之前获得对象锁的)。

      这是我的理解,不知我解释的是否清晰?

      Thumb up 1 Thumb down 0

  3. ericzhang 说道:

    嗯嗯,解释的很清晰,另外查了一下资料,再细化一下:C2,C3在C1进入get方法后会被jvm放入对象的锁池中,而P2,P3是被放入对象的等待池中,等待池的线程只有通过notify、notifyAll或者interrupt才能进入锁池中,而锁池的线程只有拿到锁标识后才进入runnable状态等待cpu时间片。

    Thumb up 2 Thumb down 0

  4. everrr 说道:

    翻译的辛苦,但是。。。
    这么关键的context不翻译?Look at producer/consumer (assumption is a ProducerConsumer class with two methods). IT IS BROKEN (because it uses notify) – yes it MAY work – even most of the time, but it may also cause deadlock – we will see why

    第一个wait() 分明是buf为full的时候才会调用,你翻译成Null。。。

    Thumb up 1 Thumb down 0

    • 宋 涛 说道:

      哦哦,确实把buf那里写错了,笔误,很抱歉!
      这个文章翻译很久了,一直存在自己的印象笔记中,当时翻译的时候没有注意到上面这段话,因为,这种情况只是可能会发生死锁,但也不是一定会发生。我再补充一下,这一点粗心了。
      谢谢提醒!
      这也让我注意到了一点,自己分享的东西也许大部分都没有人看,但是只要有人看就一定要去认真写,让读者看的舒心。谢谢!

      Thumb up 1 Thumb down 0

  5. everrr 说道:

    “消费者2(C2)将要进入同步方法中(在Y的上面),但是由于有线程在运行,所以只能被阻挡在Y处的上面。” 错,这与是否有线程运行有何关系?

    “public synchronized Object get() {
    // Y: 这里是C2获取锁的地方(在get方法之前)”
    都进入get方法了还在方法之前?

    另外你把waiting(等待中)翻译成阻塞, 阻塞是block,与等待是完全2个概念。

    Thumb up 1 Thumb down 0

    • 宋 涛 说道:

      1、对于【 “消费者2(C2)将要进入同步方法中(在Y的上面),但是由于有线程在运行,所以只能被阻挡在Y处的上面。” 错,这与是否有线程运行有何关系? 】,这样翻译如何?
      【“当消费者2(C2)正要进入同步方法的时候(此时在Y的上面,译者注:此时C2是恰好到方法处,而不是因为有线程在方法中运行才被阻挡在方法外的),生产者P1将一个对象放入到buf中,随后又调用notify方法”。】

      2、wait也翻译的有问题,wait如果没有好的翻译就直接写wait就行了。

      3、// Y: 这里是C2(试图)获取锁的地方(在get方法之前)
      这里应该理解成在get方法之前吧,或者原作者是否应该这样写?

      // Y: 这里是C2获取锁的地方
      public synchronized Object get() {
      }

      Thumb up 0 Thumb down 0

    • 宋 涛 说道:

      我对这篇文章修改了一下,您看看如何?再帮忙指点指点
      http://blog.csdn.net/tayanxunhua/article/details/20998809

      Thumb up 0 Thumb down 0

  6. 温水青蛙 说道:

    我觉得这个例子很有误导性,消费者生成者模式应该是分别单独的通知对象。否则notifyAll,等待线程都开始运行,消费者线程Object o = buf.remove(0)应该会会报错吧

    Thumb up 0 Thumb down 0

  7. 温水青蛙 说道:

    哦,有个while循环,重新等待锁,获取同步锁后再判断。

    Thumb up 0 Thumb down 0

  8. I was curious if you ever considered changing the
    layout of your site? Its very well written; I love what youve
    got to say. But maybe you could a little more in the
    way of content so people could connect with it better. Youve got an awful lot of
    text for only having 1 or two images. Maybe you could space it
    out better?

    Thumb up 0 Thumb down 0

跳到底部
返回顶部