集册 Java 并发性和多线程 嵌套管程锁死

嵌套管程锁死

欢马劈雪     最近更新时间:2020-08-04 05:37:59

70

嵌套管程锁死类似于死锁, 下面是一个嵌套管程锁死的场景:

线程 1 获得 A 对象的锁。
线程 1 获得对象 B 的锁(同时持有对象 A 的锁)。
线程 1 决定等待另一个线程的信号再继续。
线程 1 调用 B.wait(),从而释放了 B 对象上的锁,但仍然持有对象 A 的锁。

线程 2 需要同时持有对象 A 和对象 B 的锁,才能向线程 1 发信号。
线程 2 无法获得对象 A 上的锁,因为对象 A 上的锁当前正被线程 1 持有。
线程 2 一直被阻塞,等待线程 1 释放对象 A 上的锁。

线程 1 一直阻塞,等待线程 2 的信号,因此,不会释放对象 A 上的锁,
而线程 2 需要对象 A 上的锁才能给线程 1 发信号……

你可以能会说,这是个空想的场景,好吧,让我们来看看下面这个比较挫的 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 等于 false,因为线程不会继续调用 monitorObject.wait(),那么一切都没有问题 。但是如果 isLocked 等于 true,调用 lock()方法的线程会在 monitorObject.wait()上阻塞。

这里的问题在于,调用 monitorObject.wait()方法只释放了 monitorObject 上的管程对象,而与”this“关联的管程对象并没有释放。换句话说,这个刚被阻塞的线程仍然持有”this”上的锁。

(校对注:如果一个线程持有这种 Lock 的时候另一个线程执行了 lock 操作)当一个已经持有这种 Lock 的线程想调用 unlock(),就会在 unlock()方法进入 synchronized(this)块时阻塞。这会一直阻塞到在 lock()方法中等待的线程离开 synchronized(this)块。但是,在 unlock 中 isLocked 变为 false,monitorObject.notify()被执行之后,lock()中等待的线程才会离开 synchronized(this)块。

简而言之,在 lock 方法中等待的线程需要其它线程成功调用 unlock 方法来退出 lock 方法,但是,在 lock()方法离开外层同步块之前,没有线程能成功执行 unlock()。

结果就是,任何调用 lock 方法或 unlock 方法的线程都会一直阻塞。这就是嵌套管程锁死。

一个更现实的例子

你可能会说,这么挫的实现方式我怎么可能会做呢?你或许不会在里层的管程对象上调用 wait 或 notify 方法,但完全有可能会在外层的 this 上调。 有很多类似上面例子的情况。例如,如果你准备实现一个公平锁。你可能希望每个线程在它们各自的 QueueObject 上调用 wait(),这样就可以每次唤醒一个线程。

下面是一个比较挫的公平锁实现方式:

//Fair Lock implementation with nested monitor lockout problem
public class FairLock {
    private boolean isLocked = false;
    private Thread lockingThread = null;
    private List waitingThreads =
        new ArrayList();

    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()的,在方法内部有两个 synchronized 块,一个锁定 this,一个嵌在上一个 synchronized 块内部,它锁定的是局部变量 queueObject。

当一个线程调用 queueObject.wait()方法的时候,它仅仅释放的是在 queueObject 对象实例的锁,并没有释放”this”上面的锁。

展开阅读全文