Lock与Condition源码解析

Lock与Condition接口位于java.util.concurrent.locks包中,Lock接口:多线程在JDK1.5版本升级时,推出一个接口Lock接口。解决线程安全问题使用同步的形式,(同步代码块,要么同步函数)其实最终使用的都是锁机制。到了后期版本,直接将锁封装成了对象。线程进入同步就是具备了锁,执行完,离开同步,就是释放了锁。在后期对锁的分析过程中,发现,获取锁,或者释放锁的动作应该是锁这个事物更清楚。所以将这些动作定义在了锁当中,并把锁定义成对象。
所以同步是隐示的锁操作,而Lock对象是显示的锁操作,它的出现就替代了同步。
在之前的版本中使用Object类中wait、notify、notifyAll的方式来完成的。那是因为同步中的锁是任意对象,所以操作锁的等待唤醒的方法都定义在Object类中。
而现在锁是指定对象Lock。所以查找等待唤醒机制方式需要通过Lock接口来完成。而Lock接口中并没有直接操作等待唤醒的方法,而是将这些方式又单独封装到了一个对象中。这个对象就是Condition,将Object中的三个方法进行单独的封装。并提供了功能一致的方法 await()、signal()、signalAll()体现新版本对象的好处。
1. 体系结构
在java.util.concurrent包中,有两个很特殊的工具类,Condition和ReentrantLock,使用过的人都知道,ReentrantLock(重入锁)是jdk的concurrent包提供的一种独占锁的实现。ReentrantLock的一个内部类Sync继承了AbstractQueuedSynchronizer,ReentrantLock只不过是代理了该类的一些方法,可能有人会问为什么要使用内部类在包装一层? 我想是安全的关系,因为AbstractQueuedSynchronizer中有很多方法,还实现了共享锁,Condition等功能,如果直接使ReentrantLock继承它,则很容易出现AbstractQueuedSynchronizer中的API被无用的情况
2. api中方法简介
- void await():造成当前线程在接到信号或被中断之前一直处于等待状态。
- boolean await(long time, TimeUnit unit):造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
- long awaitNanos(long nanosTimeout):造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
- void awaitUninterruptibly():造成当前线程在接到信号之前一直处于等待状态。
- boolean awaitUntil(Date deadline):造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
- void signal() :唤醒一个等待线程。
- void signalAll() :唤醒所有等待线程。
2. Condition与ReentranLock使用方式
如下一个例子:

public static void main(String[] args) {
    final ReentrantLock reentrantLock = new ReentrantLock();
    final Condition condition = reentrantLock.newCondition();

    Thread thread1 = new Thread((Runnable) () -> {
            try {
                reentrantLock.lock();
                System.out.println("我要等一个新信号" + this);
                condition.await();
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("拿到一个信号!!" + this);
            reentrantLock.unlock();
    }, "waitThread1");

    thread1.start();

    Thread thread2 = new Thread((Runnable) () -> {
            reentrantLock.lock();
            System.out.println("我拿到锁了");
            try {
                Thread.sleep(3000);
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            condition.signalAll();
            System.out.println("我发了一个信号!!");
            reentrantLock.unlock();
    }, "signalThread");

    thread2.start();
}

运行后,结果如下:

我要等一个新信号lock.ReentrantLockTest$1@a62fc3
我拿到锁了
我发了一个信号!!
拿到一个信号!!

Condition的执行方式是这样的:
(1)当thread1拿到锁之后,开始执行,当调用condition.await()方法之后,thread1开始睡眠并释放锁
(2)thread1开始睡眠并释放锁之后,thread2拿到锁,拿到锁之后开始运行,并调用condition.signalAll()发射一个信号来唤醒正在等待此条件condition的线程。发射信号之后thread2会继续执行,执行完毕后thread2释放锁。
(3)当thread2释放锁之后,thread1拿到锁开始继续运行直至结束。
从上面的可以看出:Condition是一个多线程协调通信的一个工具类。使得某个或者某些线程一起等待某个条件(Condition),只有当该条件具备( signal 或者 signalAll方法被带调用)时 ,这些等待线程才会被唤醒,从而重新争夺锁。
看了上面的例子,你可能会有这样的疑问:当thread1拿到锁之后开始工作,然后调用condition.await()方法开始睡眠等待信号的达到。但是没有看见此线程释放锁呀,当thread2发出signalALL信号且释放锁之后也没有看见它重新获取锁呀??
有这样的困惑就太对了,这样才能促进我们思考嘛,是吧。
我们都知道,ReentrantLock是独占锁,一个线程拿到锁后如果不释放,那么另外一个线程肯定是拿不到锁,所以在lock.lock()和lock.unlock()之间可能有一次释放锁的操作(同样也必然还有一次获取锁的操作)。我们再回头看代码,thread1在进入lock.lock()后唯一可能释放锁的操作就是await()了。也就是说await()操作实际上就是释放锁,然后挂起线程,一旦条件满足就被唤醒,再次获取锁!
以上只是我们的猜测,下面我们就从源码的角度来分析到底await方法是如何进行了:释放锁,然后挂起线程,一旦条件满足就被唤醒以及再次获取锁等操作,我们看下ReentranLock中的方法newCondition函数,源码如下:

public Condition newCondition() {
        return sync.newCondition();
    }

这里直接使用了AbstractQueuedSynchronizer的子类Sync的newCondition方法,继续跟踪源码:

final ConditionObject newCondition() {
            return new ConditionObject();
        }

这里返回了AbstractQueuedSynchronizer的一个内部类ConditionObject(class ConditionObject implements Condition, java.io.Serializable),因此,在前面的例子中当调用condition.await方法时,就是调用的ConditionObject类中的await()方法。
我们继续跟踪源码,查看await方法的源码:

public final void await() throws InterruptedException {
            if (Thread.interrupted()) //判断当前线程是否被中断
                throw new InterruptedException();
            //将当前线程作为内容构造的节点node放入到条件队列中并返回此节点
            Node node = addConditionWaiter();  
             //释放当前线程所拥有的锁,返回值为AbstractQueuedSynchronizer的状态位(即此时有几个线程拥有锁(考虑ReentrantLock的重入))。
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            /*
                检测此节点是否在同步队列上,如果不在,说明此线程还没有资格竞争锁,此线程就继续挂起睡觉。
                直到检测到此节点在同步队列上(在上面时候加入的呢?在有线程发出signal信号的时候),
            */
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                //并检测此线程有没有被中断
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //此线程尝试的获取锁
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            //清理下条件队列中的不是在等待条件的节点
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

await方法的大概思想为:首先将此代表该当前线程的节点加入到条件队列中去,然后释放该线程所有的锁并开始睡眠,最后不停的检测AbstractQueuedSynchronizer队列中是否出现了此线程节点。如果收到signal信号之后就会在AbstractQueuedSynchronizer队列中检测到,检测到之后,说明此线程又参与了竞争锁。
await方法中用到了addConditionWaiter函数,源码如下:

private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

回到上面的例子,锁被释放后,线程1开始沉睡,这个时候线程因为线程1沉睡时调用fullyRelease方法释放锁,接着会唤醒AbstractQueuedSynchronizer队列中的头结点,所以线程2会开始竞争锁,并获取到,开始工作,线程2会调用signal方法,“发出”signal信号。我们看下signal方法的源码:

public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

方法的思想是:首先检查当前线程是否是拥有锁的独占线程,如果是则取出condition自己维护的一个链表的头结点firstWaiter,开始唤醒操作doSignal,继续跟踪:

private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

doSignal(Node first)方法干了两件事:第一件事为修改条件队列中的头结点,第二件事为完成旧的头结点的移出工作,即从Condition队列中移出到AbstractQueuedSynchronizer同步队列中去。节点的移出工作是调用transferForSignal(Node node)来完成的。transferForSignal(Node node)函数的代码如下:

final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

可以看到,正常情况 ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL) 这个判断是不会为true的,所以,不会在这个时候唤醒该线程。
只有到发送signal信号的线程调用reentrantLock.unlock()后,因为它已经被加到AQS的等待队列中,所以才可能会被唤醒