J.U.C|condition分析

一、写在前面


在前几篇我们聊了 AQS、CLH、ReentrantLock、ReentrantReadWriteLock等的原理以及其源码解读,具体参见专栏 《非学无以广才》

这章我们一起聊聊显示的Condition 对象。

二、简介


在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、wait(long timeout)、notify()、以及notifyAll 等方法可以实现等待/通知模式。

Condition接口也提供了类似于Object的监听器方法、与Lock接口配合可以实现等待/通知模式,但是两者还是有很大区别的,下图是两者的对比


object&condition对比.jpg

参考《Java并发编程艺术》

Java API 摘要

Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。

Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。

三、方法摘要


Condition提供了一系列的方法来对阻塞和唤醒线程:

  1. await():造成当前线程在接到信号或被中断之前一直处于等待状态。

  2. await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。

  3. awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。

  4. awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。

  5. awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。

  6. signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。

  7. signalAll() :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

Condition是一种广义上的条件队列。他为线程提供了一种更为灵活的等待/通知模式,线程在调用await方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。

四、具体实现


获取一个Condition必须要通过Lock的newCondition()方法。该方法定义在接口Lock下,返回的结果是绑定到此 Lock 实例的新 Condition 实例。

Condition为一个接口,其下仅有一个实现类ConditionObject,由于Condition的操作需要获取相关的锁,而AQS则是同步锁的实现基础,所以ConditionObject则定义为AQS的内部类。

public class ConditionObject implements Condition, java.io.Serializable {
    // 省略方法
}

等待队列

每个Condition对象都包含着一个FIFO队列,该队列是Condition对象通知/等待功能的关键。

在队列中每一个节点都包含着一个线程引用,该线程就是在该Condition对象上等待的线程。

我们看Condition的定义就明白了:

 public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        
        /** First node of condition queue. */
        private transient Node firstWaiter;
        
        /** Last node of condition queue. */
        private transient Node lastWaiter;

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

        // Internal methods
        // 省略方法
}

从上面代码可以看出Condition拥有首节点(firstWaiter),尾节点(lastWaiter)。

当前线程调用await()方法,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部。


等待队列的基本结构.jpg

图来源《Java 并发编程艺术》

Node里面包含了当前线程的引用。Node定义与AQS的CLH同步队列的节点使用的都是同一个类(AbstractQueuedSynchronized.Node静态内部类)。

Condition的队列结构比CLH同步队列的结构简单些,新增过程较为简单只需要将原尾节点的nextWaiter指向新增节点,然后更新lastWaiter即可。

  • 等待

调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列并释放锁。

当从await()方法返回时,当前线程一定是获取了Condition相的锁。

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;
            }
            
            // 获取同步状态
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            
            // 清理条件队列中,不实在等待状态的节点
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

此段代码的逻辑是:
首先将当前线程新建一个节点同时加入到等待队列中,然后释放当前线程持有的同步状态。

然后则是不断检测该节点代表的线程是否出现在CLH同步队列中(收到signal信号之后就会在AQS队列中检测到),如果不存在则一直挂起,否则参与竞争同步状态。

加入条件队列(addConditionWaiter())源码如下

private Node addConditionWaiter() {
            
            //队列的尾节点
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            
            // 如果该节点的状态的不是CONDITION,则说明该节点不在等待队列上,需要 清除
            if (t != null && t.waitStatus != Node.CONDITION) {
                
                // 清除等待队列中状态部位 CONDITION 的节点
                unlinkCancelledWaiters();
                
                //清除后从新获取尾节点
                t = lastWaiter;
            }
            
            // 将当前线程构造成等待节点
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            
            // 将node 节点添加到等待队列的尾部
            
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

该方法主要是将当前线程加入到Condition条件队列中。当然在加入到尾节点之前会清除所有状态不为Condition的节点。

fullyRelease(Node node),负责释放该线程持有的锁

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;
        }
    }

isOnSyncQueue(Node node):如果一个节点刚开始在条件队列上,现在在同步队列上获取锁则返回true。

final boolean isOnSyncQueue(Node node) {
        
        // 状态为CONDITION 、前驱节点为空,返回false
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
            
         // 如果后继节点不为空,则说明节点肯定在同步队列中
        if (node.next != null) // If has successor, it must be on queue
            return true;
        /*
         * node.prev can be non-null, but not yet on queue because
         * the CAS to place it on queue can fail. So we have to
         * traverse from tail to make sure it actually made it.  It
         * will always be near the tail in calls to this method, and
         * unless the CAS failed (which is unlikely), it will be
         * there, so we hardly ever traverse much.
         */
        return findNodeFromTail(node);
    }

unlinkCancelledWaiters():负责将条件队列中状态不为Condition的节点删除。

private void unlinkCancelledWaiters() {
            
            // 首节点
            Node t = firstWaiter;
            
            Node trail = null;
            // 从头开始清除状态不为CONDITION的节点
            while (t != null) {
                Node next = t.nextWaiter;
                
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

通知

调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。

public final void signal() {

            // 如果同步是以独占方式进行的,则返回 true;其他情况则返回 false  
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            
            // 唤醒首节点
            Node first = firstWaiter;
            
            if (first != null)
                doSignal(first);
        }
        

该方法首先会判断当前线程是否已经获得了锁,这是前置条件。然后唤醒条件队列中的头节点。

doSignal(Node first):唤醒头节点

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)主要是做两件事:

  • 修改头节点;

  • 调用transferForSignal(Node first) 方法将节点移动到CLH同步队列中。

transferForSignal(Node first)源码如下

final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
         // 将该节点的状态改为初始状态0
        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.prev节点
        Node p = enq(node);
        
        //如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
小结

整个通知的流程如下:

  • 判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。

  • 如果线程已经获取了锁,则将唤醒条件队列的首节点。

  • 唤醒首节点是先将条件队列中的头节点移出,然后调用AQS的enq(Node node)方法将其安全地移到CLH同步队列中 。

  • 最后判断如果该节点的同步状态是否为Cancel,或者修改状态为Signal失败时,则直接调用LockSupport唤醒该节点的线程。

五、总结


一个线程获取锁后,通过调用Condition的await()方法,会将当前线程先加入到条件队列中,然后释放锁,最后通过isOnSyncQueue(Node node)方法不断自检看节点是否已经在CLH同步队列了,如果是则尝试获取锁,否则一直挂起。

当线程调用signal()方法后,程序首先检查当前线程是否获取了锁,然后通过doSignal(Node first)方法唤醒CLH同步队列的首节点。被唤醒的线程,将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。

注:本章参考《Java 并发编程艺术》、《Java 并发编程实战》

本人技术有限,如果错误,欢迎拍砖

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

推荐阅读更多精彩内容