嵌套监视器锁定

时间:2020-01-09 10:35:51  来源:igfitidea点击:

嵌套监视器锁定如何发生

嵌套监视器锁定是一个类似于死锁的问题。发生嵌套监视器锁定,如下所示:

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实施:

//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"成员上同步。如果" isLocked"为假,则没有问题。线程不调用monitorObject.wait()。但是,如果" isLocked"为true,则将调用" lock()"的线程停放在" monitorObject.wait()"调用中。

这样做的问题是,对monitorObject.wait()的调用仅释放了monitorObject成员上的同步监视器,而不释放与" this"相关联的同步监视器。换句话说,刚刚停在等待状态的线程仍将同步锁保持在" this"上。

当首先锁定" Lock"的线程试图通过调用" unlock()"将其解锁时,试图在" unlock()方法"中输入" synchronized(this)"块将被阻止。它将一直处于阻塞状态,直到在" lock()"中等待的线程离开" synchronized(this)"块为止。但是等待lock()方法的线程不会离开该块,直到将isLocked设置为false,并执行monitorObject.notify(),就像在unlock()中那样。

简而言之,等待lock()的线程需要执行unlock()调用才能成功执行,以退出lock()和其中的同步块。但是,直到在lock()中等待的线程离开外部同步块之前,没有线程可以实际执行unlock()

结果是任何调用lock()或者unlock()的线程都会被不确定地阻塞。这称为嵌套监视器锁定。

一个更现实的例子

我们可能声称我们将永远不会实现前面显示的那种锁。我们不会在内部监控器对象上调用wait()notify(),而是在上调用。这可能是正确的。但是在某些情况下,可能会出现上述一种设计。例如,如果要在Lock中实现公平性。这样做时,我们希望每个线程在各自的队列对象上调用" 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 = waitingThreads.get(0);
      synchronized(queueObject){
        queueObject.notify();
      }
    }
  }
}
public class QueueObject {}

乍一看,这种实现看起来不错,但是请注意lock()方法如何从两个同步块内部调用queueObject.wait();。一个在" this"上同步,并嵌套其中,在" queueObject"局部变量上同步一个块。当线程调用" queueObject.wait()"时,它释放" QueueObject"实例上的锁,但不释放与" this"关联的锁。

还要注意,unlock()方法被声明为synchronized,它等于一个synchronized(this)块。这意味着,如果一个线程在" lock()"内部等待,则与" this"关联的监视对象将被等待的线程锁定。所有调用unlock()的线程将无限期地保持阻塞状态,等待正在等待的线程释放对" this"的锁定。但这永远不会发生,因为只有在线程成功向等待的线程发送信号时才会发生,并且只能通过执行unlock()方法来发送。

因此,上面的FairLock实现可能导致嵌套的监视器锁定。文本饥饿和公平中描述了公平锁定的更好实现。

嵌套监视器锁定与死锁

嵌套监视器锁定和死锁的结果几乎相同:所涉及的线程最终永远被阻塞,彼此等待。

但这两种情况并不相等。如关于死锁的文本中所述,当两个线程以不同顺序获得锁时,就会发生死锁。线程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.