天天看点

19.嵌套的监控程序锁死

嵌套的监控程序锁死是怎么样发生的

嵌套的监控程序锁死跟死锁的问题类似。一个嵌套的监控程序锁死像下面这样发生:

Thread 1 synchronizes on A
Thread 1 synchronizes on B (while synchronized on A)
Thread 1 decides to wait for a signal from another thread before continuing
Thread 1 calls B.wait() thereby releasing the lock on B, but not A.

Thread 2 needs to lock both A and B (in that sequence)
        to send Thread 1 the signal.
Thread 2 cannot lock A, since Thread 1 still holds the lock on A.
Thread 2 remain blocked indefinately waiting for Thread1
        to release the lock on A

Thread 1 remain blocked indefinately waiting for the signal from
        Thread 2, thereby
        never releasing the lock on A, that must be released to make
        it possible for Thread 2 to send the signal to Thread 1, etc.
           

这个可能听起来像是一个非常理论的场景,但是看下面的幼稚的锁实现:

//lock implementation with nested monitor lockout problem

public class Lock{
  protected MonitorObject monitorObject = new MonitorObject();
  protected boolean isLocked = false;

  public void lock() throws InterruptedException{
    synchronized(this){
      while(isLocked){
        synchronized(this.monitorObject){
            this.monitorObject.wait();
        }
      }
      isLocked = true;
    }
  }

  public void unlock(){
    synchronized(this){
      this.isLocked = false;
      synchronized(this.monitorObject){
        this.monitorObject.notify();
      }
    }
  }
}
           

注意这个lock方法首先是怎样在“this”上同步的,然后同步在monitorObject成员变量上。如果isLock是false,这里就没有问题。这个线程就不会调用monitorObject.wait方法。然而,如果是true,调用lock方法的这个线程就会被挂住等待在这个monitorObject.wait方法的调用上了。

伴随的这个问题,那个对于monitorObject.wait方法的调用只是释放了在这个monitorObject成员变量上的同步监控,而不是关联“this”的这个同步监控器。换句话说,只是被挂住等待的这个线程仍然持有在“this”的这个同步锁。

当期初锁住这个lock的这个线程尝试着去通过调用unlock方法去解锁的时候,它在尝试着进入这个unlock方法的synchronize(this)同步锁的时候将会被锁住。她将会保持锁定的直到等待在这个lock方法的这个线程离开这个synchronize(this)同步锁。但是等待在这个lock方法的这个线程不会离开那个锁直到isLocked设置为false,并且一个monitorObject.notify方法被执行,正如它发生在unlock方法中的。

将不久,正在lock方法中等待的这个线程需要一个unlock方法调用去成功的执行,为了它去离开lock方法并且它内部的同步锁。但是,没有线程可以确切的去执行unlock方法直到等待在lock方法的这个线程离开外部的同步块。

这个结果就是任何调用lock方法或者unlock方法的线程将会变得无限期的锁定的。这个就称之为嵌套的监控程序锁死。

一个更现实的例子

你可能声明你将不会像前面那样实现一个锁。你将不会在一个内部的监控对象上调用wait方法和notify方法,但是当然了,这个可能是真的。但是这里有一个场景的设计像上面的那个可能会出现。例如,如果你要在一个锁中实现公平。当这样做的时候以至于你想去调用wait方法的每一个线程在每一个他们自己的队列对象中,以至于你可以每次调用一个线程。

看这个公平锁的幼稚的实现:

//Fair Lock implementation with nested monitor lockout problem

public class FairLock {
  private boolean           isLocked       = false;
  private Thread            lockingThread  = null;
  private List<QueueObject> waitingThreads =
            new ArrayList<QueueObject>();

  public void lock() throws InterruptedException{
    QueueObject queueObject = new QueueObject();

    synchronized(this){
      waitingThreads.add(queueObject);

      while(isLocked || waitingThreads.get(0) != queueObject){

        synchronized(queueObject){
          try{
            queueObject.wait();
          }catch(InterruptedException e){
            waitingThreads.remove(queueObject);
            throw e;
          }
        }
      }
      waitingThreads.remove(queueObject);
      isLocked = true;
      lockingThread = Thread.currentThread();
    }
  }

  public synchronized void unlock(){
    if(this.lockingThread != Thread.currentThread()){
      throw new IllegalMonitorStateException(
        "Calling thread has not locked this lock");
    }
    isLocked      = false;
    lockingThread = null;
    if(waitingThreads.size() > 0){
      QueueObject queueObject = waitingThread.get(0);
      synchronized(queueObject){
        queueObject.notify();
      }
    }
  }
}
           
public class QueueObject {}
           

首先看一下这个实现可能看起来是好的,但是注意这个lock方法调用queueObject.wait方法来自于内部的两个同步块。一个同步块是“this”,并且嵌套在内部的。一个在queueObject本地变量的同步块。当一个线程调用queueObject.wait方法,它释放在QueueObject实例上的这个锁,但是不是关于“this”的这个锁。

也要注意,这个unlock方法被声明为同步的,这个跟synchronize(this)块是一样的。这就意味着,如果一个线程正在等待lock方法的内部,与this有关联的这个监控对象将会被等待的线程锁住。所有调用unlock方法的线程将会保持无休止的锁住的,去等待这个正在等待的线程去释放这个在“this”上的锁。但是这个将不会发生,因为这个只是会发生在如果一个线程成功的发送一个信号给这个等待的线程,并且这个只是在通过执行unlock方法被发送的。

因此,来自上面的公平锁实现可能会导致嵌套的监视器封锁。一个公平锁的更好的实现在饥饿和公平的文章中描述。

嵌套的监视器锁死vs.死锁

嵌套的监视器锁死和死锁的结果几乎是一样的:卷入死锁的这些线程永远会互相等待。

这两个场景虽然是不同的。像在死锁文章中描述的,当两个线程以不同的顺序获取锁,一个死锁就发生了。线程1锁住A,等待B。线程2锁住B,并且等待A。就像在死锁预防文章中描述的,死锁可以通过以相同的顺序锁住这个锁去避免。然而,一个嵌套的监视器锁死是通过两个线程以相同的顺序获取锁而发生的。线程1锁住A和B,然后释放B以及等待来自于线程2的信号。线程2需要A和B去发送线程1的信号。以至于,一个线程正在等待一个信号,并且另外一个需要一个将要被释放的锁。

这个不同,总结如下:

In deadlock, two threads are waiting for each other to release locks.

In nested monitor lockout, Thread 1 is holding a lock A, and waits
for a signal from Thread 2. Thread 2 needs the lock A to send the
signal to Thread 1.
           

翻译地址:http://tutorials.jenkov.com/java-concurrency/nested-monitor-lockout.html