• 作者: Jakob Jenkov

嵌套监控锁的锁死是如何发生的?

嵌套监控锁锁死问题类似于死锁问题。具体发生场景类似下面这个例子:

1:  Thread 1 获得A的监控器(synchronizes)
2:  Thread 1 继续获得B的监控器(synchronizes)(同时也持有者A的监控器)
3:  Thread 1 准备等待其他线程释放某个信号
4:  Thread 1 于是调用B.wait(),进行等待,但是这样,只会释放B的监控锁,而不会释放A的

5:  Thread 2 需要A和B的监控锁(注意这个顺序),才能给Thread 1 发送信号
6:  由于Thread 1 在持有A的情况下进入等待,导致Thread 2 无法获得A的监控锁
7:  Thread 2 只好直接进入锁等待,等待Thread 1 释放A

8:  Thread 1 仍然在等待B上的信号量,所以也无法释放A

上述场景描述,还是有些抽象,来看看一个具体的例子:

//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为false,说明Lock还没有被其他线程获得,所以不会获取monitorObject监控锁,所以这种情况下,是没有什么问题的。 但是,如果isLocked为true时,执行线程就会调用monitorObject.wait()

这样问题就来了,monitorObject.wait()只会释放monitorObject上的监控锁,而不会释放之前获得的this的监控锁。 也就是说,执行线程会在持有this的监控锁的情况下进入等待(parked waiting)

此时,当有线程调用Lock的unlock()方法,就会在synchronized(this)上阻塞(锁等待)。 那这样就只能期待前一个线程退出synchronized(this)代码块,但是,由于unlock()的执行线程没有机会更新isLocked,而lock()使用信号量处理的方式(循环判断内部状态,避免伪唤醒等情况),这种本来看似更加安全的执行方式,反而在这种情况下,导致锁死问题进一步加重。

这种情况,还不仅仅是上述两个线程的问题。这种情况发生时,所有后续调用lock()或者unlock()任意一个,都会导致执行线程进入锁等待。 这样就导致了嵌套监控锁的锁死问题。

一个更为实际的示例

当然,毕竟不是每个人都有机会需要自己实现一个Lock。也就很少会遇到上一节中的情况。 但是,假如需要实现一个公平锁时,就需要注意这类问题。 或者说,当你需要让每个线程在各自的等待队列中进行等待,那就需要注意这类问题。

来看看下面这个例子:

//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()是在两个嵌套的同步代码块中。 这就导致了,在queueObject.wait()时,仅仅会释放QueueObject实例的监控器锁,而不会释放this的监控器锁。

还要注意一点,unlock()方法声明为synchronized,这就等价于synchronized(this)。 这就意味着,这就潜在的存在锁死的风险(道理类似上一节中的描述)。

所以嵌套synchronized块要特别注意锁死问题,至于公平锁的一个更好的实现可以参考:《【16】并发中的饥饿问题以及公平性》

嵌套监控锁锁死与死锁的对比

嵌套监控锁锁死与死锁相同的一点就是:所有涉及的相关线程都会永久等待。

但两者并不是一个概念。 死锁简单来说,就是两个线程不同的加锁顺序导致的。 线程1持有A等待B;线程2持有B等待A。 通过【15】死锁的防范就可以避免死锁问题。 但是,嵌套监控锁锁死,则是两个线程按照相同的加锁顺序而导致的。 引发,一个线程进入条件等待,一个进入锁等待。

小结
死锁,两个线程互相等待对方的锁释放。
嵌套监控锁锁死,线程1持有锁A,并等待线程2的信号;而线程2需要锁A,才能发送信号给线程1。