ReentrantLock源码剖析三(Condition)

一、简介

       JUC中的ReentrantLock给我们提供了方便的加锁解锁操作,但是我们有时候会需要有条件的对线程进行挂起和唤醒,此时另一个工具就排上了用场。下面是Condition变量的常用用法。

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

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

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

        thread1.start();
        thread2.start();
    }

运行完之后的结果:

我要等一个新信号
我拿到锁了
我发了一个信号!!
拿到一个信号!!

       线程1获取到锁之后调用Condition的await()方法,该方法会释放锁,并将当前线程挂起。随后线程2会拿到锁,并执行signal()方法,该方法会唤起线程1,并释放锁,然后线程1拿到锁,执行后续的流程。
       所以说Condition是一个多线程间协调通信的工具,使得某个,或者某些线程一起等待某个条件(Condition),只有当该条件具备( signal 或者 signalAll方法被调用)时 ,这些等待线程才会被唤醒,从而重新争夺锁。
       那么,这些逻辑具体是怎么实现的呢?
       首先,必须要明确,在线程调用await()或者signal()/signalAll()方法时,必须首先获取锁,否则会出现java.lang.IllegalMonitorStateException异常。其次,Condition(其实是ConditionObject,Condition接口的实现)维护了一个所有等待Condition条件变量的线程的队列,每个线程构成一个Node结点,也就是AQS中的Node结点:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

二、await()

       以Condition condition = lock.newCondition();为例,newCondition()会调用Sync的newCondition()方法:

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

Sync的newCondition()方法:

final ConditionObject newCondition() {
            return new ConditionObject();
}
//ConditionObject是AQS中的内部类,实例属性只有上面的firstWaiter和lastWaiter
public ConditionObject() { }

       对于await()方法,可以抛出中断异常,

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //将当前线程封装成Node结点,添加到Condition的等待队列中
            Node node = addConditionWaiter();  
            int savedState = fullyRelease(node);   //释放所占的锁,因为在调用await()方法时,是占有锁的
            int interruptMode = 0;
            //释放了锁之后,判断当前线程的node结点是不是在syncQueue中,
            //什么是syncQueue呢?就是获取锁不成功而被挂起的线程所在的那个队列
            //如果不在syncQueue中,说明当前线程还不具备获取锁的资格,就将当前线程挂起,直到被添加到阻塞队列中,
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);  //线程被挂起,等待被signal唤醒,此时可以直接跳到下面的signal方法解析
                //挂起的过程中,如果被中断了,线程被唤醒,跳出while循环
                //如果没被中断,则此时node已经在阻塞队列中了,也会跳出循环
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //此时线程已经被唤醒,node结点已经被添加到阻塞队列中准备获取锁,被谁添加到阻塞队列了呢,是signal
            //acquireQueued尝试获取锁,被中断返回true,否则返回false
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
}

       其他版本的await()方法和这个类似,读者可以自行尝试分析。

三.signal()

//ConditionObject
public final void signal() {
            //如果在没有获取锁的情况下调用signal,会抛出IllegalMonitorStateException异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;      //拿到队列中的第一个node,此队列是Condition队列
            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);
}

//该方法是将结点移到阻塞队列中,使得当前node节点的线程可以有资格获取锁
final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        //在加入阻塞队列之前,将node的waitStatus设置为0,如果失败,说明该节点已经被取消,
        //返回false,此时上面的doSignal方法会继续遍历Condition队列,
        //找到第一个还在等待Condition变量的结点
        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).
         */
        //将结点加入到阻塞队列,返回的p结点是node的前置节点
        Node p = enq(node);
        int ws = p.waitStatus;
        //如果前一个节点已经被取消等待(ws>0),或者修改waitStatus失败,则直接唤醒。
        //正常情况 ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)这个判断
        //是不会为true的,所以,不会在这个时候唤醒该线程。
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;   //成功转移到阻塞队列,返回true
}

       那么什么时候才会唤醒呢?当前线程调用完 signal()之后,执行lock.unlock()方法之后,释放锁的时候,会唤醒其他线程,这个就是unlock()的逻辑了。
       signalAll()方法的逻辑与signal()类似,读者可以自己分析一遍。

四.总结

       现在我们再来理一遍:
       一个线程Alock.lock()获取锁成功之后,调用condition.await()方法,该方法会首先将线程封装成node加入Condition队列,然后释放锁,将线程移出阻塞队列,然后挂起;另一个线程B因为之前线程释放锁,获取锁成功,调用signal()方法,将Condition队列的第一个node转移到阻塞队列,这个时候还没完,执行完signal之后会释放锁,此时会唤醒后面的线程,此时线程A有机会竞争到锁。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容