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并发编程的艺术》方腾飞

 

 

posted @ 2020-05-17 22:54  蜀中孤鹰  阅读(413)  评论(0编辑  收藏  举报