- 作者: 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。