从ReentrantLock源码深入理解Condition

1,811 阅读8分钟

前言

上一篇文章从ReentrantLock源码深入理解AQS我们通过分析ReentrantLock源码了解其实现原理,理解了AQS的思想,这次我们再以这种方式来解析Condition,我们的入口还是ReentrantLock类(别再解析我了,都被扒光了。。)

Condition简介

Condition其实是J.U.C下的一个接口,最主要的三个方法是await()、signal()、signalAll(),其功能和Object类的wait()、notify()、notifyAll()类似,根据JDK源码的注释大致翻译下每个方法功能:

await()

释放当前Condition对象关联的锁,当前线程会一直等待直到下列四种情况发生:

  • 其他线程调用了此Condition对象的signal()方法,并且刚好选中当前线程进行唤醒
  • 其他线程调用了此Condition对象的signalAll()方法
  • 其他线程中断了当前线程(Thread#interrupt方法)
  • 发生“虚假唤醒”(我还没太明白这个意思)

在以上所有情况下,在当前线程从await()方法返回之前,当前线程必须重新获取该Condition对象关联的锁。

根据上述说明,可以总结几个关键点:

1.Condition对象是和一个锁对象关联的

2.调用Condition.await(),一旦这个方法执行完成返回后,当前线程一定重新获取到了锁

signal()

唤醒一个等待的线程

signalAll()

唤醒所有等待的线程

从ReentrantLock看Condition

初始化

从上文可以知道,Condition对象一定是一个锁对象关联起来的,例如,当我们使用ReentrantLock时,如何获取一个Condition对象呢?答案是调用ReentrantLock.newCondition()方法。

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    private final Sync sync;
    
    public Condition newCondition() {
        return sync.newCondition();
    }

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
        
        final ConditionObject newCondition() {
            return new ConditionObject();
        }
    }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** 条件队列中的第一个节点. */
        private transient Node firstWaiter;
        /** 条件队列中的最后节点. */
        private transient Node lastWaiter;

        /**
         * Creates a new {@code ConditionObject} instance.
         */
        public ConditionObject() { }
    }
}

从源码可以看到,调用ReentrantLock.newCondition(),实际是调用其内部类Sync的一个实例属性的sync.newCondition(),这个方法是调用ConditionObject的无参构造方法,而ConditionObject是AQS类的一个内部类,它实现了Condition接口。所以,类继承和调用关系图如下:

newCondition调用关系

Condition使用

我们先通过一个用例来看下Condition的使用:

public class MyLock extends AbstractQueuedSynchronizer {

    static ReentrantLock lock = new ReentrantLock();

    static Condition condition = lock.newCondition();

    static volatile boolean flag = false;


    public static class Thread1 implements Runnable{

        @Override
        public void run() {
            lock.lock();
            System.out.println("线程1获取到锁");

            try{
                System.out.println("线程1执行逻辑");
                while (!flag){
                    System.out.println("条件不满足,线程1挂起");
                    condition.await();
                    System.out.println("线程1被唤醒");
                }
                System.out.println("条件满足,线程1继续执行");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
                System.out.println("线程1执行完毕");
            }

        }
    }

    public static class Thread2 implements Runnable{

        @Override
        public void run() {
            try {
                //先等待2s,保证线程1先获取锁
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            lock.lock();
            System.out.println("线程2获取到锁");

            try{
                System.out.println("线程2执行逻辑");
                flag = true;
                condition.signal();
                //关注点:让线程2睡眠5s
                Thread.sleep(5000);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
                System.out.println("线程2执行完毕");
            }

        }
    }

    public static void main(String[] args) {
        new Thread(new Thread1()).start();
        new Thread(new Thread2()).start();
    }
}

执行结果:

线程1获取到锁
线程1执行逻辑
条件不满足,线程1挂起
线程2获取到锁
线程2执行逻辑
线程2执行完毕
线程1被唤醒
条件满足,线程1继续执行
线程1执行完毕

Process finished with exit code 0

执行逻辑很简单,线程1先获取到锁,线程2获取锁失败进入等待,线程1发现条件不满足后,调用await()进行挂起,释放锁,然后线程2获取锁成功,将标志位flag置为true后,将线程1唤醒,线程2执行完毕后释放锁,然后线程1重新获取锁,发现条件满足后跳出循环执行完毕,最后释放锁。

注意代码中有个关注点,线程2调用signal()方法后,进行了5s的休眠,为什么要这么做,是为了突出之前说的两点:

1.调用signal方法只是唤醒一个线程,仅仅是唤醒,但锁还在当前线程手中

2.调用await方法的线程,当此方法执行完毕返回后,当前线程一定重新获取到了锁

所以我们看到,当线程2调用signal()方法然后休眠后,控制台没有新的内容打印,线程1并没有继续执行,因为此时线程2并没有释放锁,所以线程1虽然被唤醒,但仍在await方法中,直到线程2执行完成后,线程1才打印出“线程1被唤醒”。

接下来我们看看await()方法的源码实现,准确说,是ConditionObject对await()的实现:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** 条件队列中的第一个节点. */
        private transient Node firstWaiter;
        /** 条件队列中的最后节点. */
        private transient Node lastWaiter;

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //加入到等待队列中
            Node node = addConditionWaiter();
            //完全释放锁,并返回之前的锁状态,标识着重入次数
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //只要不在同步队列中,就一直循环
            while (!isOnSyncQueue(node)) {
                //挂起当前线程
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //尝试获取锁,传入的state是之前保存的值,即保证重入次数和之前一致
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                //去除等待队列中状态不是Condition的节点
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
        
        /**
         * Adds a new waiter to wait queue.
         * @return its new wait node
         */
        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;
        }
    }
        
    /**
     * Invokes release with current state value; returns saved state.
     * Cancels node and throws exception on failure.
     * @param node the condition node for this wait
     * @return previous sync state
     */
    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }
    
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
        
        return findNodeFromTail(node);
    }
}

await方法首先是将当前线程封装成Node加入到等待队列中,创建Node对象时传入的waitStatus是Node.CONDITION,表示在等待队列中,在这里我们发现,AQS中有两个队列:同步队列(sync queue)和等待队列(condition queue,或者叫条件队列),同步队列就是我们上一篇文章从ReentrantLock源码深入理解AQS中提到的,当线程获取锁失败后进入的队列,两个队列中用的节点都是Node对象,根据Node.waitStatus来区分是在哪个队列中(对同一个对象,改变一个属性值就可以标识在不同的队列中,的确设计的很巧妙)。加入等待队列后,当前线程会释放锁,对于可重入锁,这里会重入次数保存,方便下次重新获取锁后保持重入次数与之前一致。然后判断当前线程是否在同步队列中,此时Node.waitStatus=Node.CONDITION,因此进入循环体中,当前线程挂起,等待被唤醒,后续流程我们等分析完signal()方法后再继续。

        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
        
        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
        
        /**
        * Transfers a node from a condition queue onto sync queue.
        * Returns true if successful.
        * @param node the node
        * @return true if successfully transferred (else the node was
        * cancelled before signal)
        */
        final boolean transferForSignal(Node node) {
        /*
         * 如果设置状态失败,说明当前节点已经是取消状态了
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //将当前节点入队到同步队列中
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

线程2调用signal()方法后,会获取等待队列中的头结点,将头结点的waitStatus从CONDITION设置为0,然后加入到同步队列中,并把同步队列中的这个节点的前一节点状态置为SIGNAL,表示它的下一个节点等待被唤醒,如果前一节点waitStatus=CACELLED或者设置为SIGNAL失败,就会直接唤醒当前线程。

signal()方法很简单,执行完后我们回到await()方法挂起的地方,被唤醒后由于waitStatus=0,所以isOnSyncQueue(node)返回true,跳出循环,然后调用acquireQueued方法尝试获取锁,acquireQueued之前我们已经分析过,如果当前节点不是同步队列的头结点(准确说同步队列的头结点是一个空节点,这里的头结点指的第一个等待被唤醒的节点),则又会挂起进行等待,所以这时其实线程1还在await方法中,因为它没有获取到锁,直到线程1在同步队列中排队等到了并获取到锁后,acquireQueued返回,然后await方法才执行完毕。

到此,Condition的await和signal分析完毕。

小结

从AQS的实现逻辑可以看出,当线程获取锁失败后,会进入同步队列中等待,而当获取锁的线程调用await方法后,会释放锁并进入等待队列中等待,当线程从等待队列中被唤醒后,会进入到同步队列中继续等待,直到成为同步队列中的头结点后,才能获取到锁继续执行。

Last but not least

码字不易,给个点赞在走吧,与君共勉~~