摘录自 极客时间

破坏占用且等待条件时候,如果转出账本和转入账本不满足同时在文件架上这个条件,用死循环的方式来循环等待,核心代码如下:

// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target))
  ;

如果apply()操作耗时非常短,而且并发冲突量不大时,这个方案还挺不错的,因为这种场景下,可能要循环上万次才能取到锁,比较消耗CPU。
其实在这种场景下,最好的方案应该是: 如果线程要求的条件不满足,则线程阻塞自己,进入等待状态,满足再通知线程重新执行。这样可以避免CPU消耗。

等待-通知机制

完整的等待-通知机制是线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待的线程,重新获取互斥锁。

Java中synchronized配合wait()notify()notifyAll()可以实现等待-通知机制。

wait():

  • 等待队列和互斥锁是一对一的关系,每个互斥锁都有自己独立的等待队列。
  • 线程在进入等待队列的同时,会释放持有的互斥锁,线程释放锁以后,其他线程就有机会获得锁,并进入临界区了。
    p1
    要注意这里的左右两个等待队列不是一个等待队列,唤醒的时候也是从右边的队列去唤醒

notify()notifyAll():

  • 当条件满足时,会通知互斥锁的等待队列中的线程,告诉它条件曾经满足过。所谓曾经,是因为notify()只能保证在通知时间点条件满足。而线程真正开始执行的时候可能已经被别人横刀夺爱了。
  • 被通知的线程想要重新执行的时候,仍然需要获取到互斥锁(因为在wait的时候被释放了)。

PS: wait()notify()notifyAll()方法操作的等待队列是互斥锁的等待队列,如果synchronized锁定的this,那么对应的一定是this.wait()this.notify()this.notifyAll()。如果是target,那么都是target.,而且三个方法都要提前获得互斥锁。因此,这三个方法都要在
synchronized{}内部调用。如果在外部调用,或者锁定了this去调用target.,都会通知异常。

下面代码是一个经典写法。

class Allocator {
  private List<Object> als;
  // 一次性申请所有资源
  synchronized void apply(Object from, Object to){
    // 经典写法
    while(als.contains(from) || als.contains(to)){
      try{
        wait();
      }catch(Exception e){}   
    } 
    als.add(from);
    als.add(to);  
  }
  // 归还资源
  synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
    notifyAll();
  }
}

要注意的是要尽量使用notifyAll()notify()会随机地通知等待队列中的一个线程,但是notifyAll()会通知等待队列中的所有线程。notify()看起来比notifyAll()友好,但是notify()可能导致某些线程永远不会被通知到。

假设有资源A、B、C、D,线程1申请到了AB,线程2申请到了CD,此时线程3申请AB就会进入等待队列,线程4申请CD也会进入等待队列。
假设线程1归还了AB,使用notify()通知,有可能被通知的是线程4,但线程4申请的是CD,所以线程4还是会继续等待,线程3就再也起不来了。

思考小题

waitsleep的区别

  • wait会释放资源,但是sleep不释放资源
  • wait需要被唤醒,sleep不需要被唤醒
  • wait需要获取监视器,否则抛出异常,sleep不需要
  • wait是object顶级父类方法,sleep是Thread方法