Basic Of Concurrency(十二: Nested Monitor Lockout)

Nested Monitor Lockout的发生

Nested Monitor Lockout问题相似于死锁,一个Nested Monitor Lockout的发生以下:html

线程1 持有A对象锁进入同步块
线程1 持有B对象锁进入同步块(当成功持有A对象锁后)
线程1 调用B.wait()释放B对象锁,但不释放A对象锁
线程1 等待另外一个线程(调用B.notify())发送信号以继续执行释放锁A

线程2 须要同时持有A对象锁和B对象锁才能给线程1发送信号
线程2 没法获取A对象锁,由于A对象锁已经被线程1持有
线程2 无限期的等待线程1释放A对象锁
线程1 无限期的等待线程2发送信号,以唤醒继续执行;由于线程2只有在持有A对象锁的状况才有办法给线程1发送唤醒信号
复制代码

上面的描述看起来有点抽象,接下来咱们来看一下一个简陋SimpleLock的实现:java

public class SimpleLock {
    private class Monitor {

    }

    private final Monitor monitor = new Monitor();
    private boolean isLocked = false;

    public void lock() throws InterruptedException {
        synchronized (this) {
            while (isLocked) {
                synchronized (monitor) {
                    monitor.wait();
                }
            }
            isLocked = true;
        }
    }

    public void unLock() {
        synchronized (this) {
            isLocked = false;
            synchronized (monitor) {
                monitor.notify();
            }
        }
    }
}
复制代码

能够注意到在lock()方法中的第一个synchronized构造块中传入的是"this"。第二个synchronized构造块中传入的是成员变量monitorObject。当isLocked为false并不会有任何问题,线程不会调用到monitor.wait()。但当isLocked为true时,则线程会调用到while()循环内部的monitor.wait()进入等待状态。post

这里的问题在于调用完monitor.wait()方法后,线程只释放了monitorObject对象锁,并无释放“this”即当前SimpleLock实例对象锁。SimpleLock实例对象锁仍然被第一个线程持有。this

当线程须要给lock()方法中的monitor.wait()发送信号时,它须要尝试获取this即当前SimpleLock实例对象锁来进入synchronized(this)同步代码块。此时它会无限期的等待着,由于SimpleLock实例对象锁一直被第一个线程持有且永远不会释放。由于第一个线程释放SimpleLock实例对象锁须要另外一个线程给它发送唤醒信号来退出wait()方法和退出synchronized(this)同步代码块。spa

简而言之就是第一个线程在lock()方法的同步代码块中等待着另外一个线程发送信号好让它退出同步代码块。另外一个发送信号的线程则须要第一个线程退出lock()方法的同步代码块好让它进入unLock()方法中的同步代码块来发送信号给第一个线程。线程

最终的结果是不管哪一个线程调用lock()和unLock()方法都会无限期的等待下去。这种问题咱们称之为Nested Monitor Lockout。设计

一个更加真实的例子

你也许会以为你永远不会像上文说起的例子那样来实现一个Lock,即不会在一个使用对象锁的同步代码块中调用wait()和notify(),但事实上这是真实发生的。当你设计的代码相似于上文说起实例中所遇到的状况时,问题就会发生了。如,在上一篇文章中实现一个FairLock的时候。当你但愿每一个线程都去调用队列中与之一一对应的对象的wait()方法,且在日后的时间里去调用该对象的notify()以此来唤醒对应的线程的时候。code

一个公平锁的简单实现:htm

public class FairLock {
    private class QueueObject {
    }

    private boolean isLocked = false;
    private Thread lockingThread;
    private List<QueueObject> waitingThreads = new ArrayList<>();

    public void lock() throws InterruptedException {
        // 为每个线程建立与之一一对应的对象锁
        QueueObject queueObject = new QueueObject();
        // 线程得到当前对象实例锁进入同步代码块
        synchronized (this) {
            // 将当前线程对应的对象锁添加到等待队列中
            waitingThreads.add(queueObject);
            // 判断当前FairLock是否为锁住对象,且判断等待队列中队头是否为当前线程对应的对象锁
            while (isLocked || waitingThreads.get(0) != queueObject) {
                // 得到当前线程对应的对象锁进入同步代码块
                synchronized (queueObject) {
                    try {
                        // 阻塞当前线程进入等待状态
                        queueObject.wait();
                    } catch (InterruptedException e) {
                        // 若线程意外唤醒,则从等待队列中移除
                        waitingThreads.remove(queueObject);
                        throw e;
                    }
                }
            }
            // 当前FairLock,当前线程是第一个进入该同步代码块的线程
            // 移除本线程在等待队列中对应的对象锁
            waitingThreads.remove(queueObject);
            // 取得FairLock锁
            isLocked = true;
            lockingThread = Thread.currentThread();
        }
    }

    public synchronized void unLock() {
        // 检查异常状况,当前线程并不是持有FairLock线程
        if (lockingThread != null && lockingThread != Thread.currentThread()) {
            throw new IllegalMonitorStateException("Calling thread has not locked this lock");
        }
        // 释放锁
        isLocked = false;
        lockingThread = null;
        // 等待队列中有其余线程在等待取得FairLock
        if (waitingThreads.size() > 0) {
            // 取得队列头部线程与之对应的对象锁,
            final QueueObject queueObject = waitingThreads.get(0);
            // 取得该对象锁
            synchronized (queueObject) {
                // 唤醒对象锁对应的线程
                queueObject.notify();
            }
        }
    }
}
复制代码

第一眼感受这个实现没什么问题,但咱们能够注意处处在lock()方法两个synchronized()代码块中调用queueObject.wait()的部分;实际上线程在执行完queueObject.wait()后仅会释放synchronized(queueObject)所持有的queueObjct对象锁,并不会释放synchronized(this)所持有的"this"即FairLock实例对象锁。对象

一样须要注意的是unLock()方法签名中声明有synchronized关键字,等同于synchronized(this)代码块。这意味着若是一个线程在lock()方法中的synchronized(this)代码块无限期的等待下去,那么其余线程将会被无限期的阻塞。调用unLock()方法的线程也会无限期的阻塞等待其余线程释放synchronized(this)所持有的锁以进入unLock()方法。一旦线程没法进入unLock()方法就没法给持有synchronized(this)对象锁的线程发送信号让它退出等待状态(退出wait()方法)从而退出synchronized(this)代码块。

可见,上文FairLock的实现可以带来Nested Monitor Lockout问题。一个改进的FairLock实现已经在饥饿与公平中说起。

Nested Monitor Lockout vs 死锁

从结果看Nested Monitor Lockout和死锁的状况十分相似:线程都会终结于互相等待彼此到永远。

固然它们也不是那么的类似。在线程死锁与预防中咱们提到死锁会在两个线程以不一样顺序获取相同的锁的状况下发生。线程1持有锁A,尝试获取锁B。线程2持有锁B,尝试获取锁A。一样在线程死锁与预防中,咱们提到可让线程在获取相同锁的时候以相同顺序的方式进行,以此来解决死锁问题。但实际上,Nested Monitor Lockout问题就是按照顺序来获取相同的锁。线程1持有锁A和锁B而且等待线程2发送信号。线程2须要获取锁A和锁B来给线程1发送信号。因此状况变成了一个线程在等待另外一个线程发送信号,另外一个线程则在等待它释放锁。

如下对两种状况进行描述:

在死锁中,两个线程互相等待对方释放本身所须要的锁。

在Nested Monitor Lockout中,线程1持有锁A,等待线程2发送信号。线程2须要锁A来给线程1发送信号。
复制代码

该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: 饥饿与公平
下一篇: Slipped Conditions

相关文章
相关标签/搜索