CyclicBarrier source code analysis of multithreaded programming

CyclicBarrier source code analysis how to kill CountDownLatch

What are Cyclic Barriers?

CyclicBarrierCommonly known as 栅栏barriers, it is a multi-threaded synchronization tool.

It is often compared CountDownLatchwith , because both are tools for waiting for a group of threads.

Different from the coordinating thread CountDownLatchthat is often used to wait for a group of worker threads, and the worker threads will be notified once and continue to execute after they arrive CyclicBarrier. As the name suggests, a group of threads wait for each other to arrive at the designated place before continuing to execute.

CyclicBarrierMore advanced would be:

  • Reuse is allowed, and can be reused after execution or manual reset; CountDownLatchdirectly scrapped.

  • Callback logic is allowed to be executed, usually the last thread to reach the fence calls itself. We can use this feature to perform finishing touches in our business logic.

  • If there is a problem, the current generation can be destroyed (notify other threads), and reset for the next time. It is different from not caring about the result after CountDownLatchreaching the specified condition, CyclicBarrierand must wait on the fence. If a worker thread gets an exception before it arrives, it will need to be manually reset.

    Of course, abnormalities, timeouts, etc. will automatically destroy the current use, but note that it cannot be used directly for the next time, and must be reset manually.

How to use Cyclic Barrier?

CountDownLatch: A safe with several locks (juejin.cn) cue CyclicBarriersays that after opening the safety lock, it can no longer be used.

image-20210621163638007

Allow reuse

Of course, the cronies hope that after the boss unlocks and takes the money and runs away, the boss will not find out when he comes back to check.image-20210621164945886

private static void normal() throws InterruptedException {
    
    
    CyclicBarrier barrier = new CyclicBarrier(3);
    if (!barrier.isBroken()) {
    
    
        System.out.println("保险箱:安全保护中");
    }
    System.out.println("亲信们:不成功便成仁!!!!!");
    for (int i = 0; i < 3; i++) {
    
    
        Thread thread = new Thread(new MyFollower(barrier));
        thread.start();
    }
    Thread.sleep(1000);
    if (!barrier.isBroken()) {
    
    
        System.out.println("保险箱:安全保护中");
        System.out.println("老板:很好,这东西不错");
    }
}

private static class MyFollower implements Runnable {
    
    
    private CyclicBarrier barrier;

    public MyFollower(CyclicBarrier barrier) {
    
    
        this.barrier = barrier;
    }

    @Override
    public void run() {
    
    
        System.out.println("亲信:输入密码ing");
        try {
    
    
            barrier.await();
        } catch (InterruptedException e) {
    
    
            System.out.println("临死前:骂骂咧咧地退出了游戏");
        } catch (BrokenBarrierException e) {
    
    
            System.out.println("OS:傻子,这都输入错了");
        }
        System.out.println("亲信:成了!!!!!");
    }
}
保险箱:安全保护中
    
亲信们:不成功便成仁!!!!!
    
亲信:输入密码ing
亲信:输入密码ing
亲信:输入密码ing
    
亲信:成了!!!!!
亲信:成了!!!!!
亲信:成了!!!!!
    
保险箱:安全保护中
老板:很好,这东西不错
In the case of a trusted friend being killed (interruption), wrong input (voluntary destruction), or slow input (timeout), what does it matter to my safety lock?

Since internal thread interruption, or active destruction, checks a boolean value, other threads cannot know what caused the destruction in this case.

image-20210621174527946

Simple code implementation (the real situation definitely needs more perfect coordination and processing)

private static void normal() throws InterruptedException {
    
    
    CyclicBarrier barrier = new CyclicBarrier(3);
    if (!barrier.isBroken()) {
    
    
        System.out.println("保险箱:安全保护中");
    }
    System.out.println("亲信们:不成功便成仁!!!!!");
    for (int i = 0; i < 3; i++) {
    
    
        Thread thread = new Thread(new MyFollower(barrier, i + 1));
        thread.start();
        if (i == 0) {
    
    
            thread.interrupt();
        }
    }
    Thread.sleep(2000);
    if (barrier.isBroken()) {
    
    
        System.out.println("亲信:重置一下");
        barrier.reset();
    }

    if (!barrier.isBroken()) {
    
    
        System.out.println("保险箱:安全保护中");
        System.out.println("老板:很好,这东西不错");
    }
}

private static class MyFollower implements Runnable {
    
    
    private CyclicBarrier barrier;
    private int no;

    public MyFollower(CyclicBarrier barrier, int no) {
    
    
        this.barrier = barrier;
        this.no = no;
    }

    @Override
    public void run() {
    
    
        System.out.println("亲信:输入密码ing");
        if (no == 3) {
    
    
            System.out.println("亲信:输错了...重置一下");
            barrier.reset();
            return;
        }
        if (no == 2) {
    
    
            System.out.println("亲信:输慢了...");
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            return;
        }
        try {
    
    
            barrier.await(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
    
    
            System.out.println("临死前:骂骂咧咧地退出了游戏");
            return;
        } catch (BrokenBarrierException e) {
    
    
            System.out.println("OS:哪个傻子错了啊");
            return;
        } catch (TimeoutException e) {
    
    
            System.out.println("亲信:是谁输入慢了");
            return;
        }
        System.out.println("亲信:成了!!!!!");
    }
Manufacturer's custom unlock result (callback function)

Manufacturers of safety locks allow consumers to customize celebrations, notifications, etc. after unlocking. How to do it?

image-20210621175156339

private static void normal() throws InterruptedException {
    
    
    CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    
    
        System.out.println("保险箱:恭喜主人,又拿到钱了!!!");
    });
    if (!barrier.isBroken()) {
    
    
        System.out.println("保险箱:安全保护中");
    }
    System.out.println("亲信们:不成功便成仁!!!!!");
    for (int i = 0; i < 3; i++) {
    
    
        Thread thread = new Thread(new MyFollower(barrier));
        thread.start();
    }
    Thread.sleep(1000);
    if (!barrier.isBroken()) {
    
    
        System.out.println("保险箱:安全保护中");
        System.out.println("老板:很好,这东西不错");
    }
}
保险箱:安全保护中
    
亲信们:不成功便成仁!!!!!
    
亲信:输入密码ing
亲信:输入密码ing
亲信:输入密码ing
    
亲信:成了!!!!!
亲信:成了!!!!!
亲信:成了!!!!!
保险箱:恭喜主人,又拿到钱了!!!
    
保险箱:安全保护中
老板:很好,这东西不错

Note: The callback function can do anything, depending on how it is used. But this is executed by the last worker thread to reach the fence.

CyclicBarrier source code

CyclicBarrierLet's consider it under the AQS system, but it does not directly inherit and extend AQS CountDownLatchinternally , but uses the same internally extended AQS ReentrantLock.

About AQS and ReenrtantLockcan view:

Based on this, we speculate about the internal structure CyclicBarrierof :

  • There must be a lock, in order to ensure the concurrency of multiple threads
  • Conditional, for collective waiting at the fence
  • There must be a counter to calculate whether all have arrived
  • To have the number of participants, used to reset the counter
  • Must have a broken state
  • There must be a generational measure to ensure that the reset counter and state will not affect the previous one.
  • To save the callback function.
properties and inner classes
private static class Generation {
    
    
    Generation() {
    
    }                 // prevent access constructor creation
    // 标识栅栏破坏,线程不会再等待
    // 一般几种情况:
    // 1.等待超时
    // 2.线程中断
    // 3.手动重置
    // 4.回调执行异常
    boolean broken;                 // initially false
}
// 用于控制对状态、数量操作的并发控制
private final ReentrantLock lock = new ReentrantLock();
// 线程等待在栅栏处的条件变量
private final Condition trip = lock.newCondition();
// 参与线程的数量
private final int parties;
// 完成时的回调动作,由最后一个到达栅栏的线程执行
private final Runnable barrierCommand;
// 允许重用下的分代
private Generation generation = new Generation();

// 剩下还没达到等待栅栏的线程数量, 每次到达就 --
// 初始值为 parties;
// parties - count = 正在等待的线程数量
private int count;

Comparative speculation:

  • There must be a lock ReentrantLock lock, in order to ensure the concurrency of multiple threads
  • Conditional Condition trip, for collective waiting at the fence
  • There must be a counter countto calculate whether all have arrived
  • To have the number of participants parties, used to reset the counter
  • Must have a broken stategeneration.broken
  • There must be a generational measure generationto ensure that the reset counter does not affect the previous one.
  • To save the callback function barrierCommand.

However Generation, the generational processing of is relatively simple, and will not retain other states of the previous generation, and will be reset directly; only the damaged state is retained, which is used to limit the exit of threads that are still in the execution process of the previous generation.

So the reset and notify methods are as follows:

Note: To avoid confusion, generally internal calls to these methods must only require locks

into the next generationnextGeneration

Being able to successfully enter the next generation means that all have successfully reached the barrier, and the callback function has been executed successfully.

// 进入下一个分代
private void nextGeneration() {
    
    
    // signal completion of last generation
    // 唤醒上一个分代中的等待线程
    trip.signalAll();
    // 重置 count 计数
    count = parties;
    generation = new Generation();
}
flag vandalism

The general situation is interruption, timeout, execution failure, etc.

private void breakBarrier() {
    
    
    generation.broken = true;
    count = parties;
    // 唤醒当前分代中的等待线程
    trip.signalAll();
}

It can be seen that the two operations of notifying the thread must be accompanied by the reset countof , the difference is that generationthe and are operated respectively twice broken.

Explain that when a thread waits for the fence, it will judge two values ​​according to different situations.

Constructor
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);
}
core method
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));
}

It can be seen that the waiting method is called internally dowait. Judging from the exception list of the method, it is a large and comprehensive processing method that can run out of interruption, destruction, and timeout exceptions.

dowait
/**
* @param timed 是否允许超时
* @param nanos 超时时间, 只有 timed = true  才有意义
* @return 线程到达栅栏的索引: 第一个:parties - 1; 最后一个:0
*/
private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    
    
    final ReentrantLock lock = this.lock;
    // 1.核心逻辑得加锁
    lock.lock();
    try {
    
    
        final Generation g = generation;

        if (g.broken)
            // 2.如果已经破坏, 必须手动重置才能使用
            throw new BrokenBarrierException();

        if (Thread.interrupted()) {
    
    
            // 3.提前检查当前执行线程已经中断了, 那收尾:破坏栅栏,顺便唤醒所有等待线程
            breakBarrier();
            throw new InterruptedException();
        }

        int index = --count;
        if (index == 0) {
    
      // 4.全部到达了,当前线程是最后一个
            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
        // 5.循环等待,直到中断、破坏、超时的任意情况发生
        for (;;) {
    
    
            try {
    
    
                // 判断是超时或不超时等待
                if (!timed)
                    trip.await();
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
    
    
                // 6.判断是外部还是内部中断
                if (g == generation && ! g.broken) {
    
    
                    // 6.1说明是外部其他线程中断的
                    // 进行收尾的同时, 得抛出异常
                    breakBarrier();
                    throw ie;
                } else {
    
    
                    // 6.2如果进行到这里,那肯定是属于栅栏被破坏,或者全部到达栅栏
                    // 直接恢复中断状态,不需要处理异常
                    Thread.currentThread().interrupt();
                }
            }

            if (g.broken)
                // 7。检查是不是属于被破坏唤醒
                throw new BrokenBarrierException();

            if (g != generation)
                // 8。是否结束了, 相当于返回序号
                return index;

            // 9.没结束,判断是否超时:超时收尾
            if (timed && nanos <= 0L) {
    
    
                breakBarrier();
                throw new TimeoutException();
            }
            // 我也不清除啥情况会继续循环.
            // 如果会循环, 那中断状态会被延续到下一轮的第3步检查中断,从而抛出中断异常
        }
    } finally {
    
    
        lock.unlock();
    }
}

Have you noticed that the first step is to acquire the lock first, because whether it is a counter operation, or nextGeneration, breakBarrieror entering a blocked state, it is guaranteed by the lock

In step 6, it may be interrupted while waiting, depending on the situation, whether it is an external interrupt or an internal interrupt: nextGenerationbreakBarrier

6.1 If it is an external interrupt, destroy the fence and throw an exception.

6.2 If it is an internal interruption, it will only restore the interruption state and proceed to the next step. Because the general interruption belongs to the end of the wait, no exception will be thrown.

6.2 Subsequent judgment of timeout, destruction, and next generation (end or reset)

reset

Reset will break the current generation wait, so it will be called breakBarrier.

Enter the next generation: nextGeneration.

If the reset is only for the next generation, the latter can be called directly; but the waiting of the current generation must be interrupted.

So combine the two only different operations in the sum breakBarrier:nextGeneration

  • generation.broken = true;
  • generation = new Generation();
public void reset() {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        // 唤醒等待线程, 标记已破坏
        breakBarrier();   // break the current generation
        // 进入下次分代
        nextGeneration(); // start a new generation
    } finally {
    
    
        lock.unlock();
    }
}
statistical methods

The main purpose is to judge whether it is damaged and to obtain the number of waiting threads.

Combined getParties, you can also know the number of remaining unreached threads.

public boolean isBroken() {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        return generation.broken;
    } finally {
    
    
        lock.unlock();
    }
}
public int getNumberWaiting() {
    
    
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    
    
        return parties - count;
    } finally {
    
    
        lock.unlock();
    }
}
public int getParties() {
    
    
    return parties;
}

Note that the number of threads calling the method is inconsistent awaitwith the initial settingparties

  1. Less than parties, cannot countreduce to 0, will eventually cause all execution awaitthreads to be blocked.
  2. More than parties, just partiesafter all arrived, entered the next generation. In the next generation, the number of threads is more than partiesthe number, causing this part of the thread to block.

It is recommended that the number of threads must be equal to the number of threads parties, and try to call awaitthe method .

Summarize

The examples about CountDownLatchand CyclicBarrierare not very good, but they can barely convey the meaning.

To sum up, the three major crimes of CyclicBarrierJuan's death CountDownLatch:

  • Reuse is allowed, and can be reused after execution or manual reset; CountDownLatchdirectly scrapped.

  • Callback logic is allowed to be executed, usually the last thread to reach the fence calls itself. We can use this feature to perform finishing touches in our business logic.

  • If there is a problem, the current generation can be destroyed (notify other threads), and reset for the next time. It is different from not caring about the result after CountDownLatchreaching the specified condition, CyclicBarrierand must wait on the fence. If a worker thread gets an exception before it arrives, it will need to be manually reset.

    Of course, abnormalities, timeouts, etc. will automatically destroy the current use, but note that it cannot be used directly for the next time, and must be reset manually.

Guess you like

Origin blog.csdn.net/jiangxiayouyu/article/details/118108001