• CyclicBarrier源码深入剖析


     

    1 前言

    CyclicBarrier是一种同步工具,它允许一组线程在到达一个公共的屏障点时阻塞等待,直到最后一个线程到达屏障点,屏障才能开启,此时所有被阻塞线程才能被唤醒从而继续执行。CyclicBarrier是一个可循环利用(cyclic)的的屏障(barrier),与CountDownLatcher相比的不同之处在于,它可以重置屏障的次数,它可以在释放等待线程之后重新使用。(基于JDK1.8)

    2 用法示例

    CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

    class CyclicBarrierTest {
        static CyclicBarrier c = new CyclicBarrier(2);
    ​
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    System.out.println("子线程到达屏障点");
                    c.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("所有线程均到达屏障点后,子线程打印" + 1);
            }).start();
            try {
                System.out.println("主线程到达屏障点");
                c.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("所有线程均到达屏障点后,主线程打印" + 2);
        }
    }

    因为主线程和子线程的调度是由CPU决定,所以字符串“所有线程均到达屏障点后,子线程打印1“、”所有线程均到达屏障点后,主线程打印2“的输出先后顺序不固定。但”子(主)线程到达屏障点“ 打印输出一定先于”所有线程均到达屏障点后,子(主)线程打印1(2)“,因为CyclicBarrier规定“只有所有的线程都到达屏障点时,这些被阻塞线程才能继续执行”。

    如果将CyclicBarrier的构造方法参数改为“new CyclicBarrier(3)”,字符串“所有线程均到达屏障点后,子(主)线程打印1(2)”一直不会被输出,因为构造方法指定了3个线程,我们实际上只在两个线程中使用了“c.await()”,这样永远不可能有3个线程都达到屏障点的时机,这两个线程将一直被阻塞等待。

     

    另外CyclicBarrier还有一个带有两个参数的构造方法CyclicBarrier(int parties, Runnable barrierAction),一个表示屏障拦截的线程数,另一个是Runnable类型参数barrierAction 。此barrierAction 在最后一个线程到达屏障点之后但在唤醒所有线程之前被执行。换句话说,在线程到达屏障时,优先执行barrierAction 。此屏障操作可用在任何一线程继续执行之前更新共享状态。

    class CyclicBarrierTest {
        static CyclicBarrier c = new CyclicBarrier(2,()->{
           String tName= Thread.currentThread().getName();
          String gName=  Thread.currentThread().getThreadGroup().getName();
            System.out.println("Thread '" + tName +"' in thread group '" +gName+"' executes barrier action.");
        });
    ​
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    System.out.println("子线程到达屏障点");
                    c.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("所有线程均到达屏障点后,子线程打印" + 1);
            }).start();
            try {
                System.out.println("主线程到达屏障点");
                c.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("所有线程均到达屏障点后,主线程打印" + 2);
        }
    }

    可以看出barrierAction先于“xxxxxxx1(2)”输出,再次验证了“在线程到达屏障时,优先执行barrierAction”。

     

    3 成员变量与构造方法

        /** The lock for guarding barrier entry */
        private final ReentrantLock lock = new ReentrantLock();
        /** Condition to wait on until tripped */
        private final Condition trip = lock.newCondition();
        /** The number of parties */
        private final int parties;
        /* The command to run when tripped */
        private final Runnable barrierCommand;
        /** The current generation */
        private Generation generation = new Generation();
        /**
         * Number of parties still waiting. Counts down from parties to 0
         * on each generation.  It is reset to parties on each new
         * generation or when broken.
         */
        private int count;

    lock:排他锁,保证数据访问安全。

    trip:与屏障相关的条件。

    parties:屏障拦截的线程数,这个值是常量初始化后不再变化。

    barrierCommand:在所有线程到达屏障点时优先执行的任务。

    count:当前需要阻塞等待的线程数。

    generation:线程的中断状态相关。Generation是一个静态内部类,这只有一个布尔类型的成员变量broken,broken表示线程的中断。

    private static class Generation {
        boolean broken = false;
    } 

     

    构造方法CyclicBarrier(int,barrierAction)主要涉及对各实例变量的初始化,对实例变量count初始为parties,当前没有任何线程被阻塞,所以counts的初值设为屏障拦截的线程数。

    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }
        public CyclicBarrier(int parties) {
            this(parties, null);
        }

    4 主要方法

    (1) await

    await方法使当前线程阻塞等待。这里的两个await方法的核心逻辑都委托给行dowait方法,带参的await方法对阻塞时间做了限制,这两个await方法都响应中断。

    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);//不做超时限制
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }
        public int await(long timeout, TimeUnit unit)
            throws InterruptedException,
                   BrokenBarrierException,
                   TimeoutException {
            return dowait(true, unit.toNanos(timeout));
        } 

     

    dowait方法是屏障拦截功能的主要实现方法。

    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            final Generation g = generation;
    ​
            if (g.broken)
                //generation在初始化时,broken是false,而这之前又没有代码对g.broken更改
                //如果出现true,表示其他方法它置为了false,这里抛出异常。
                throw new BrokenBarrierException();
    ​
            if (Thread.interrupted()) {
                //如果当前线程是中断的,就执行breakBarrier(记录中断,重置count,并唤醒所有等待线程),
                breakBarrier();
                throw new InterruptedException();
            }
    ​
            int index = --count;//多一个线程阻塞,将需要阻塞等待的线程数count自减1
           //若所有线程均到达屏障点,准备执行command,然后方法返回
            if (index == 0) {  // tripped ,
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;//command正常完成,未发生异常
                    nextGeneration();//生成新的Generation.
                    return 0;
                } finally {
                    if (!ranAction)//执行command发生异常,执行breakBarrier
                        breakBarrier();
                }
            }
                //自旋等待,此时线程就被阻塞了。
            //只有在 所有线程到达屏障点 或Generation broken或线程中断 或等待超时,才能退出for循环
            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    if (!timed)
                        trip.await();//未设置超时就不限时的休眠等待。(同时会释放锁)
                    else if (nanos > 0L)//设置了超时,当前还未超时,就超时休眠等待。
                        nanos = trip.awaitNanos(nanos);//(同时会释放锁)
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        //generation未被其他线程修改,且未broken就执行breakBarrier
                        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();
                    }
                }
                //从trip.awaitXX中返回了(超时或被trip.signalXX方法唤醒)
    if (g.broken)
                    throw new BrokenBarrierException();
    ​
                if (g != generation)
                    //generation被其他线程修改了,表示barrier被重置或所有线程都到达屏障点,
                    //解除线程的阻塞,方法返回
                    return index;
    ​
                if (timed && nanos <= 0L) {//超时时间已过,执行breakBarrier,然后抛出异常,方法返回
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

    dowait的整个方法体被使用锁lock保护起来,保证数据访问安全。

    ①dowait先检查成员变量generation的状态,如果是broken就抛出BrokenBarrierException异常。Generation是用来记录屏障拦截过程中的线程是否中断、有无其他异常发生、及屏障是否被重置等信息。

    ② 如果当前线程是中断的(Thread.interrupted()),就执行Generation.breakBarrier,并抛出InterruptedException。

    breakBarrier的主要逻辑是记录中断 、重置count 、并唤醒所有休眠的线程

    private void breakBarrier() {
        generation.broken = true;//中断
        count = parties;//重置count为parties
        trip.signalAll();//将所有休眠的线程从Condition.await方法中唤醒返回
    }

    ③将当前需要阻塞等待的线程数count自减。

    ④若count自减后为0了,表示所有线程均到了达屏障点,就执行command任务(command.run()),并执行nextGeneration()产生下一个Genenatrion(方便CyclicBarrier下次重新使用),然后方法返回。。nextGeneration的方法逻辑简单,唤醒所有休眠的线程、重置count、创建一个新的Generation。

    private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
    }

    若执行command任务过程中发生了异常,就执行breakBarrier()

    ⑤若count自减后不为0了,表示还有线程未到达屏障点,此时需要进入for死循环自旋,从此时起当前线程就被阻塞了。只有当所有线程到达屏障点或Generation broken或线程中断或等待超时,才能退出for循环,方法才能得到返回。 for循环的核心逻辑:

    若未设置超时就不限时地休眠等待(trip.await());若设置了超时且当前还未超时,就超时休眠等待(nanos = trip.awaitNanos(nanos))。

    在休眠超时后或被trip.sinaglXXX方法唤醒后,进行一系列的条件检测,确定是否可以退出自旋。

    • 如果Generation broken,就抛出异常BrokenBarrierException,方法结束。

    • 若 generation被修改了(g != generation),表示CyclicBarrier被重置或所有线程都已到达屏障点,就方法返回,解除线程的阻塞状态。

    • 若超时时间已过(timed && nanos <= 0L),执行breakBarrier,然后抛出异常,方法结束。

    若以上三个条件均不满足,就会继续自旋。

    (2) getNumberWaiting

    getNumberWaiting方法返回当前正被阻塞等待的线程数。

    public int getNumberWaiting() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return parties - count;//屏障拦截的线程总数-当前还需阻塞的线程数
        } finally {
            lock.unlock();
        }
    } 

    (3) isBroken

    isBroken查询当前是否broken状态

    public boolean isBroken() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return generation.broken;
        } finally {
            lock.unlock();
        }
    }

    (4) getParties

    getParties方法返回越过屏障所需的线程数。parties是final常量,一经初始化后便不再变化,是一个只读的共享变量,它不存在线程安全问题,不需要用锁来保证数据访问安全。

    public int getParties() {
        return parties;
    } 

    (5) reset

    reset方法重置barrier的初始状态。如果当前还有线程在屏障点阻塞等待,则有可能抛出BrokenBarrierException异常。这里因为breakBarrier方法将generation.broken置为true,若reset方法还未执行到nextGeneration方法时,此时dowait的for自旋中又恰好检测到generation的broken为true就抛出BrokenBarrierException异常。

    public void reset() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            breakBarrier();   // break the current generation
            nextGeneration(); // start a new generation
        } finally {
            lock.unlock();
        }
    } 

    5 工作流程

    假设一个项目中用代码CyclicBarrier c= new CyclicBarrier(3)构造一个CyclicBarrier对象, 阻塞等待用的是非超时版本的c.await(),那么这里c.count的初始值就是3。

    ①当第一个线程执行到代码片段c.await()进入到dowait方法中,dowait方法首先要尝试获取锁lock,由于它是第一个线程,此时没有线程竞争能立即获取到锁lock。获取到锁后,将当前需要阻塞等待的线程数count自减1,(count初始为3)此时count自减后为2(不为0),所以它会进入for循环,它一进入for自旋就执行trip.await(),当前(第一个)线程就休眠并释放锁lock .

    ②当第二个线程执行到代码片段c.await()进入到dowait方法中,dowait方法首先要尝试获取锁lock,由于第一个线程在休眠后释放了锁lock,所以这个线程也能立即获取到锁。获取到锁后,将当前需要阻塞等待的线程数count自减1,此时count自减后为1,同样不为0,所以它也会进入for循环,它一进入for自旋也立即执行trip.await(),当前(第二个)线程线程就休眠并释放锁lock .

    ③当第三个线程执行到代码片段c.await()进入到dowait方法中,dowait方法首先要尝试获取锁lock, 由于第二个线程在休眠后释放了锁lock,所以此线程也能立即获取到锁。获取到锁后,将当前需要阻塞等待的线程数count自减1,此时count自减后为0,方法进入代码块if (index == 0){...}内部,若有barrierCommand,就先执行barrierCommand任务(由此可见,barrierCommand任务会在最后一个到达屏障点的线程中执行),之后再执行方法nextGeneration(),然后从dowait方法return返回。可以看出第三个(最后一个到达屏障点的)线程执行到c.await()不会休眠等待。

    nextGeneration()方法很关键,此方法体中的trip.signalAll()将唤醒前两个(所有)线程,使得前两个线程从trip.await()的休眠中返回,继续执行for循环接下来的代码。在接下来的for循环代码中检测到if (g != generation)条件成立(nextGeneration方法将重新创建一个Generation对象,并将引用赋给成员变量generation),从而从dowait方法中return返回,结束阻塞状态,这两个线程得以继续执行。

     

    参考: 《Java并发编程的艺术》方腾飞

     

  • 相关阅读:
    Web应用程序使用Hibernate
    Hibernate使用注释
    Hibernate入门程序
    Hibernate体系结构
    Spring MVC文件上传教程
    Spring MVC配置静态资源和资源包教程
    Spring MVC4使用Servlet3 MultiPartConfigElement文件上传实例
    Spring4 MVC文件下载实例
    Spring4 MVC+Hibernate4 Many-to-many连接表+MySQL+Maven实例
    Spring4 MVC+Hibernate4+MySQL+Maven使用注解集成实例
  • 原文地址:https://www.cnblogs.com/gocode/p/analysis-source-code-of-CyclicBarrier.html
Copyright © 2020-2023  润新知