嵌套监视器锁定
嵌套监视器锁定如何发生
嵌套监视器锁定是一个类似于死锁的问题。发生嵌套监视器锁定,如下所示:
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.