CyclicBarrier内部机制

前言

如果我们希望所有的线程都到达同一个地方才能继续往下执行,那么CyclicBarrier就是一个不错的选择。

开始

在下面一个例子中,我希望10个线程都要到达同一个地方才可以往下走。

    public static void main(String[] args) {
        int num = 10;
        CyclicBarrier cb = new CyclicBarrier(num);
        for (int i = 0; i < num; i++) {
            Thread t = new Thread(new Task(cb, i + 1));
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t.start();
        }
    }
    static class Task implements Runnable {
        private final CyclicBarrier cb;
        private final int id;

        Task(CyclicBarrier cb, int id) {
            this.cb = cb;
            this.id = id;
        }

        @Override
        public void run() {
            try {
                System.out.println("Task#" + id + " is waiting...");
                cb.await();
                System.out.println("Task#" + id + " is finished...");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

结果如下所示:

Task#1 is waiting...
Task#2 is waiting...
Task#3 is waiting...
Task#4 is waiting...
Task#5 is waiting...
Task#6 is waiting...
Task#7 is waiting...
Task#8 is waiting...
Task#9 is waiting...
Task#10 is waiting...
Task#10 is finished...
Task#1 is finished...
Task#3 is finished...
Task#4 is finished...
Task#5 is finished...
Task#6 is finished...
Task#2 is finished...
Task#8 is finished...
Task#9 is finished...
Task#7 is finished...

可以看见,线程一个接一个启动了,一旦第十个线程到达了前九个线程到达的地方,那么每个线程都启动了,启动后的输出就没有什么顺序了。这是CycicBarrier一个比较简单的例子。再利用jstack可以看到线程等待的状态:

"Thread-0" #12 prio=5 os_prio=0 tid=0x00007f7438136800 nid=0x3dc4 waiting on condition [0x00007f7411de2000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000d6d7ce38> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:234)
        at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
        at com.jdk.CyclicBarrierDemo$Task.run(CyclicBarrierDemo.java:39)
        at java.lang.Thread.run(Thread.java:745)

这就很清楚了,从调用深度高到低排序,LockSupport的park方法,AQS里的ConditionObject的await方法,CyclicBarrier的dowait方法,找它们错不了。

CyclicBarrier的dowait方法

dowait方法这个描述很形象,一下子就告诉了我们,我就是让线程抱团停下的方法,有什么事找我吧。

    // 这是CyclicBarrier提供给我们调用的API
    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

    /**
     * Main barrier code, covering the various policies.
     */
    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        //毕竟是临界区,安全起见,还是要上锁了,
        //每个new出来的CyclicBarrier对象就有一把唯一(final)的重入锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //CyclicBarrier是可以重用的,它的重用机制就是通过设置一个Generation实现
            //当我们希望重用这个CyclicBarrier对象,reset()调用以后
            //generation属性就被重新创建。
            final Generation g = generation;

            //这段代码是想说,所有的线程还没到达同一个地方,
            //CyclicBarrier就被“无效”了(broken),那是不正常的,要抛异常
            if (g.broken)
                throw new BrokenBarrierException();

            //线程被设置了中断标签,这是符合突破这个CyclicBarrier的条件了,
            //所以,调用breakBarrier()没毛病,但是还是要抛出异常
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            //每次拿到锁的线程走到这里都要对计数count--
            //虽然count--有三个步骤要走,但是毕竟上锁了,who cares
            int index = --count;
            if (index == 0) {  // tripped
                //走到这里,CyclicBarrier就真的被翻越了
                boolean ranAction = false;
                try {
                    //按照文档的说法,我们可以设置一个Runnable
                    //在CyclicBarrier被翻越的时候执行
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    //设置新的Generation已提供再用。
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    //没有超时,调用ConditionObject的await或者awaitNanos方法,当前线程被阻塞
                    //至于拿到的锁,会在await或者awaitNanos方法里面释放
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }
                //以下代码和上面说的一样,总结一下就是
                //如果Thread的中断状态位被设置了就抛出InterruptedException
                //或者在还有线程等待的时候CyclicBarrier被翻越BrokenBarrierException
                if (g.broken)
                    throw new BrokenBarrierException();

                if (g != generation)
                    return index;
                //等待超时,也要抛异常,这次是TimeoutException
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            //解锁好习惯
            lock.unlock();
        }
    }

AQS里的ConditionObject的await方法

    //这段代码还是在CyclicBarrier里面
    //线程等待的入口就是通过这个实现了Condition接口的trip来完成的
    //是通过ReentrantLock获取的,这也是它能够释放锁的伏笔了。
    private final Condition trip = lock.newCondition();
    
        //以下这个方法才是主角
        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //先加入一个waitStatus=CONDITION的Node
            //这个Node就是AQS里面使用的CLH队列的Node
            //加入的是条件(condition)队列,而不是同步队列(sync)
            Node node = addConditionWaiter();
            //释放锁的地方就在这里了,要知道,ReebtrantLock继承了AQS,
            //这实际上是在一个类里面操作,还要返回这个线程上了几次锁,
            //也就是AQS里state属性的值,保存在savedState,
            //给突破CyclicBarrier以后,争取锁使用
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //只要不在同步队列中,当前线程就要调用LockSupport的park方法
            //调用park方法的线程会阻塞
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                //等于0表示没有cancel,还继续要等待
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //尝试把node加入阻塞队列,加入阻塞队列就是获取公平锁的机制了
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            //Condition队列用的是nextWaiter指针,属于单向链表
            //在这里要清理已经取消等待的线程
            //unlinkCancelledWaiters使用的思路就是我们在单向链表删除节点的思路,
            //从头遍历,改变nextWaiter指针。
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            //不等于0,确实是需要中断,给线程设置中断标志位,或者抛出异常
            //这可以在reportInterruptAfterWait方法立细究
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
    

唤醒线程

    private void breakBarrier() {
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }

所以,现在就主要看ConditionObject的signalAll的方法了。

ConditionObject的signalAll方法

        public final void signalAll() {
            //如果不是异己锁,抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                //将节点脱离条件队列,就是将nextWaiter指针置空
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                //把脱离条件队列的一个接一个节点加入同步队列,并唤醒
                transferForSignal(first);
                first = next;
            } while (first != null);
        }
        
        final boolean transferForSignal(Node node) {
        /*
         * 把节点的状态值从CONDITION设置为0
         * 0的意义就是,他不属于CONDITION,CANCELED,SIGNAL这三种状态,属于等待状态。
         * 如果设置失败,就只能返回false了
         */
        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;
        // !compareAndSetWaitStatus(p, ws, Node.SIGNAL)这句话的意思是说,重设waitStatus可能会暂时无害的错误
        // ws > 0代表Node的状态是CANCELLED
        // 整体来说就是,如果Node状态是CANCELLED或者把Node的状态设为SIGNAL失败,
        // 就把线程唤醒,也让他去争取锁,源码上写这是暂时的,无害的错误
        // 在AQS的acquireQueued方法里面,没有获取到锁会一直卡在那个方法里面
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

后记

总结起来就是,要想让规定数量的线程都达到同一个点才开始执行,就得让线程等待,计数。
AQS的条件队列和同步队列的设计正是用的这种思想,线程等待,就加入条件队列,要释放线程,因为还要获取锁才能越过CyclicBarrier的await方法,所以要加入同步队列获取锁。

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

推荐阅读更多精彩内容