并发编程-08之CyclicBarrier

在这里插入图片描述

一 认识CyclicBarrier
1.1 什么是CyclicBarrier
字面意思回环栅栏(循环屏障),通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。

1.2 CyclicBarrier的常用方法
构造方法
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
● parties表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
● barrierAction用于在线程到达屏障时,优先执行 barrierAction,方便处理更复杂的业务场景(该线程的执行时机是在到达屏障之后再执行) 。
● count 用于重置屏障数。
常用方法
//BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
public int await() throws InterruptedException, BrokenBarrierException{…};
public int await(long timeout, TimeUnit unit){…}
1.3 CyclicBarrier 应用场景
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
假设我们有三个线程去统计三个同学的成绩,一个线程负责统计三个同学平均分
代码演示:
@Slf4j
public class CyclicBarrierDemo1 {

//保存每个学生的平均成绩
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String,Integer>();

private ExecutorService threadPool= Executors.newFixedThreadPool(3);

private CyclicBarrier cb = new CyclicBarrier(3,()->{
    int result=0;
    Set<String> set = map.keySet();
    for(String s:set){
        result+=map.get(s);
    }
    log.info("三人平均成绩为{}分",result/3);
});

public void count(){
    log.info("开始统计三个同学的成绩");
    for(int i=0;i<3;i++){
        int finalI = i;
        threadPool.execute(new Runnable(){
            @Override
            public void run() {
                //获取学生平均成绩
                int score=(int)(Math.random()*40+60);
                map.put("同学"+ finalI, score);
                log.info("同学"+ finalI +"的成绩为"+score);
                try {
                    //执行完运行await(),等待所有学生平均成绩都计算完毕
                    cb.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }

        });
    }
}

public static void main(String[] args) {

    CyclicBarrierDemo1 cb=new CyclicBarrierDemo1();
    cb.count();

}

}
运行结果:

利用CyclicBarrier的计数器能够重置,屏障可以重复使用的特性,可以支持类似“人满发车”的场景
这里以田径比赛为例,每轮五个选手,选手准备好后,裁判发号施令。
@Slf4j
public class CyclicBarrierDemo2 {

public static void main(String[] args) {

    AtomicInteger counter = new AtomicInteger();
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            5, 5, 1000, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100),
            (r) -> new Thread(r, counter.addAndGet(1) + " 号 "),
            new ThreadPoolExecutor.AbortPolicy());

    CyclicBarrier cyclicBarrier = new CyclicBarrier(5,
            () -> {
        log.info("裁判:比赛开始~~");

    });

    for (int i = 0; i < 10; i++) {
        threadPoolExecutor.submit(new Runner(cyclicBarrier));
    }

}

}
@Slf4j
class Runner extends Thread{
private CyclicBarrier cyclicBarrier;
public Runner (CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}

@Override
public void run() {
    try {
        int sleepMills = ThreadLocalRandom.current().nextInt(1000);
        Thread.sleep(sleepMills);
        log.info(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms" + cyclicBarrier.getNumberWaiting());
        cyclicBarrier.await();
        log.info(Thread.currentThread().getName() + " 选手开跑 " );
    } catch (InterruptedException e) {
        e.printStackTrace();
    }catch(BrokenBarrierException e){
        e.printStackTrace();
    }
}

}
运行结果:

1.4 CyclicBarrier和CountDownLatch的区别
● CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
● CyclicBarrier还提供getNumberWaiting(可以获得CyclicBarrier阻塞的线程数量)、isBroken(用来知道阻塞的线程是否被中断)等方法。
● CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
● CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同。CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,再执行。CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行。
● CyclicBarrier 还可以提供一个 barrierAction,合并多线程计算结果。
● CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞唤醒的,而CountDownLatch则是通过AQS的“共享锁”实现。
二 CyclicBarrier原理
我们需要关注的点有:
1.一组线程在触发屏障之前互相等待,最后一个线程到达屏障后唤醒逻辑是如何实现的
2.删栏循环使用是如何实现的
3.条件队列到同步队列的转换实现逻辑
这里记录一下CyclicBarrier内部核心成员
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
CyclicBarrier的核心方法就是await(),我们直接顺着await()方法慢慢往下看
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
dowait(false, 0L)
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)
throw new BrokenBarrierException();
//不重要的逻辑,略过
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
//计数减1
int index = --count;
//这里是到达屏障后才会调用
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
//正常阻塞的地方
// 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) {
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();
}
}

            if (g.broken)
                throw new BrokenBarrierException();

            if (g != generation)
                return index;

            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

当计数器没有到达屏障的时候,会调用trip.await(),也就是进入条件队列中
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);
}
在await()方法中,第一步就是通过addConditionWaiter()构建条件队列,并加入到条件队列中
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
这里线程按照先后顺序,创建单链表的条件队列,加入条件队列之后,调用fullyRelease(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;
}
}
释方完锁就会进入whie条件中,判断是否在同步队列中,然后阻塞在这里
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
以上是未达到屏障,线程阻塞的逻辑,接下来一个线程过来,刚好达到屏障,我们看看重新回到dowait()方法d x
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
这里减1之后,index为0,可以看见这里会先调用入参的Runnable接口,然后才会去唤醒其他线程,接着我们看看nextGeneration()方法做了什么?
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
这里可以看出,我们的循环栅栏先唤醒索引的线程然后重置了计数,这就是它能复用的原理,我们看下它是如何唤醒阻塞的线程的。
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 {
//获取当前节点的下一个节点
Node next = first.nextWaiter;
//当前节点值的下个节点置空,方便回收
first.nextWaiter = null;
//条件队列转同步队列
transferForSignal(first);
first = next;
} while (first != null);
}
这里通过一个do_while,进入条件队列转同步队列的迁移
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
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;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

看到enq方法是不是很熟悉,就是我们构建同步队列的方法,
private Node enq(final Node node) {
for (;😉 {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
在这里,通过循环把条件队列的节点转移到同步队列中
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
但是唤醒阻塞的线程不是在这里唤醒的,这里的逻辑是把waitStatus从0改为-1,我们接着往下看dowait方法的finally,其实在到达屏障的线程释放独占锁依次唤醒同步队列中的线程。
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)
            throw new BrokenBarrierException();

        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }

        int index = --count;
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                //执行完毕
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }


        }
    } finally {
        lock.unlock();
    }
}

这里写自定义目录标题

  • 19
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值