[JUC] source code analysis of CyclicBarrier

Posted by pete07920 on Fri, 14 Jan 2022 15:59:18 +0100

CyclicBarrier

1. Introduction

Loop fence is used for thread cooperation and waiting for threads to meet a certain count. When constructing, set the count number. When each thread executes to a time when synchronization is required, call the await() method to wait. When the number of waiting threads meets the count number, continue to execute.

@Slf4j
public class TestCyclicBarrier {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);

        CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
            log.debug("task1,task2 finish...");

        });

        //When the count becomes 0, await is initialized to its original value in the next call
        for (int i = 0; i < 3; i++) {
            service.submit(() -> {
                log.debug("task1 begin...");
                Sleeper.sleep(1);
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
            service.submit(() -> {
                log.debug("task2 begin...");
                Sleeper.sleep(2);
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }
        service.shutdown();
    }
}

result:

2. Internal class

Generation, translated into Chinese, is the generation of one generation, which is used to control the recycling of CyclicBarrier. CountDownLatch cannot do this. CountDownLatch is one-time and cannot be reset times.

private static class Generation {
    boolean broken = false;
}

3. Main attributes

//Reentry lock
private final ReentrantLock lock = new ReentrantLock();
//Conditional lock, named trip, means tripping. It may mean that the thread trips first when it comes, and then wakes up when it reaches a certain number
private final Condition trip = lock.newCondition();
//Number of threads to wait
private final int parties;
//A command executed when awakened
private final Runnable barrierCommand;
//generation
private Generation generation = new Generation();
//Number of threads to wait for in the current generation
private int count;

4. Construction method

The constructor needs to pass in a parties variable, that is, the number of threads to wait.

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    //Initialize parties
    this.parties = parties;
    //Initialize count equal to parties
    this.count = parties;
    //Initializes commands that are executed when they reach the fence
    this.barrierCommand = barrierAction;
}

5.await() method

Each thread that needs to wait at the fence needs to explicitly call the await() method to wait for the arrival of other threads.

public int await() throws InterruptedException, BrokenBarrierException {
    try {
        //Call the dowait method without timeout
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}
private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
TimeoutException {
    final ReentrantLock lock = this.lock;
    //Lock
    lock.lock();
    try {
        //Current generation
        final Generation g = generation;
        //inspect
        if (g.broken)
            throw new BrokenBarrierException();
        //Interrupt check
        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }
        //The value of count minus 1
        int index = --count;
        //If the number is reduced to 0, follow this logic (the last thread goes here)
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                //If a command is sent during initialization, execute it here
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                //Call next generation method
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // This loop can only be reached by a non last thread
        for (;;) {
            try {
                if (!timed)
                    //Call the await() method of condition
                    trip.await();
                else if (nanos > 0L)
                    //Timeout wait method
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    Thread.currentThread().interrupt();
                }
            }
            //inspect
            if (g.broken)
                throw new BrokenBarrierException();
            //Normally, it's definitely not equal here
            //Because the reference of generation has changed when the nextGeneration() method is called when the fence is broken
            if (g != generation)
                return index;

            //Timeout check
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}
private void nextGeneration() {
    // Call signalAll() of condition to transfer all the waiters in its queue to the AQS queue
    trip.signalAll();
    // Reset count
    count = parties;
    // Enter the next generation
    generation = new Generation();
}

The whole logic in the dowait() method is divided into two parts:

  1. The last thread follows the above logic. When the count is reduced to 0, it breaks the fence. It calls the nextGeneration() method to notify the waiting thread in the condition queue to transfer to the AQS queue to wait to be awakened and enter the next generation.
  2. The non last thread follows the following for loop logic. These threads will block at the await() method of condition. They will join the condition queue and wait to be notified. When they wake up, they have been updated and replaced. At this time, they return.

6. Summary

  1. CyclicBarrier will block a group of threads at await(). When the last thread arrives, it will wake up (just transfer from the condition queue to the AQS queue) the threads in front of it, and everyone will continue to move on
  2. CyclicBarrier is not a synchronizer implemented directly using AQS
  3. CyclicBarrier implements the whole synchronization logic based on ReentrantLock and its Condition

What are the similarities and differences between CyclicBarrier and CountDownLatch?

  1. Both can block a group of threads waiting to be awakened
  2. The former wakes up automatically when the last thread arrives
  3. The latter is achieved by explicitly calling countDown()
  4. The former is realized through Reentry lock and conditional lock, and the latter is directly based on AQS
  5. The former has the concept of "generation" and can be reused, while the latter can only be used once
  6. The former can only realize that multiple threads can run together when they reach the fence
  7. The latter can not only realize that multiple threads wait for one thread condition to be established, but also realize that one thread waits for multiple thread conditions to be established

Topics: Java Back-end source code JUC