Java concurrency -- detailed source code of cyclicbarrier and CountDownLatch

Keywords: Programming Java IE less

Summary

CountDownLatch and CyclicBarrier are similar, and they are often compared. This time, the author tries to analyze the two classes respectively from the source point of view, and from the source point of view, see the differences between the two classes.

CountDownLatch

CountDownLatch is literally a counting tool class. In fact, this class is a JAVA method for multithreading counting.

The internal implementation of CountDownLatch mainly relies on AQS sharing mode. When a thread initializes a count for CountDownLatch, other threads will block the call to await. Until other threads call the countDown method one by one to release, reduce the value of count to 0, that is, release the synchronization lock, and await will go down.

Sync

Internally, it mainly implements a synchronizer Sync inherited from AQS. The Sync source code is as follows:

    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        // Construction method, parameter is the value of count
        Sync(int count) {
            // Internal use of state to store count
            setState(count);
        }

        // Get the value of count
        int getCount() {
            return getState();
        }

        // Try to get share mode synchronizer
        protected int tryAcquireShared(int acquires) {
            // Judge the value of state. If it is 0, it will succeed. Otherwise, it will fail
            // Inherited from AQS, according to the comments in AQS, we can know if the result is returned
            // If it is greater than 0, it indicates success. If it is less than 0, it indicates failure
            // 0 will not be returned here because it has no meaning
            return (getState() == 0) ? 1 : -1;
        }

        // Release synchronizer
        protected boolean tryReleaseShared(int releases) {
            // Optional operation
            for (;;) {
                // Get state
                int c = getState();
                // If state is 0, return false directly
                if (c == 0)
                    return false;
                // Calculate the result of state-1
                int nextc = c-1;
                // CAS operation synchronizes this value to state
                if (compareAndSetState(c, nextc))
                    // If the synchronization is successful, judge whether the state is 0 at this time
                    return nextc == 0;
            }
        }
    }

Sync is a synchronizer inherited from AQS. The following points are worth discussing in this Code:

  1. Why use state to store the value of count?

    Because state and count are actually a concept. When state is 0, resources are idle. When count is 0, all CountDownLatch threads have been completed. So although they are not of the same significance, their performance at the code implementation level is completely consistent. Therefore, count can be recorded in state.

  2. Why does tryAcquireShared not return 0?

    First of all, we need to explain the possible return values of tryAcquireShared in AQS: a negative number indicates that the shared lock cannot be acquired, a 0 indicates that the shared lock can be acquired, but the current thread has occupied all the shared lock resources after acquiring, and the next thread will not have any extra resources to acquire, a positive number indicates that you can acquire the shared lock, and there is a margin after that Shared locks can be provided to other threads. Then let's go back to tryAcquireShared in CountDownLatch. We don't pay attention to the follow-up threads and the subsequent resource usage. As long as I have the current status, the return value of 0 is unnecessary.

  3. Why are the parameters in tryreleased not used?

    According to the implementation of this class, we can know that the parameter of tryReleaseShared must be 1, because the completion of the thread must be one by one. In fact, when we look at the internal call of countDown method to sync.releaseShared method, we can see that it has a parameter of 1 written dead, so in fact, the reason why the parameter in tryreleased is not used is because the parameter value is fixed to 1

Constructors and methods

    // Construction method
    public CountDownLatch(int count) {
        // count must be greater than 0
        if (count < 0) throw new IllegalArgumentException("count < 0");
        // Initialize Sync
        this.sync = new Sync(count);
    }


    // Waiting to acquire lock (can be interrupted)
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    // Wait to acquire lock (delay)
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    // Counter down (release synchronizer)
    // 1 reduction per call
    public void countDown() {
        sync.releaseShared(1);
    }

    // Get count
    public long getCount() {
        return sync.getCount();
    }

    // toString
    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }

CyclicBarrier

CyclicBarrier is literally a circular fence. Its role in JAVA is to let all threads wait after they are finished, until all threads are finished, and then carry out the next operation.

Instead of directly inheriting AQS to realize synchronization, CyclicBarrier uses ReentrantLock and Condition to complete its internal logic.

Member variable

    // lock
    private final ReentrantLock lock = new ReentrantLock();

    // condition
    private final Condition trip = lock.newCondition();

    // Number of threads
    private final int parties;

    // The Runnable method executed after all threads are executed, which can be empty
    private final Runnable barrierCommand;

    // Grouping
    private Generation generation = new Generation();

    // Number of unfinished threads
    private int count;

    private static class Generation {
        boolean broken = false;
    }

We can see that there is a very strange class Generation in the member variable, which is a static class declared by CyclicBarrier internally. Its function is to help distinguish the grouping and Generation of threads, so that CyclicBarrier can be reused. If this simple explanation can't make you understand it well, you can see the following source code analysis, and understand its purpose through implementation.

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);
    }

It's a very normal constructor. It's just a simple initialization of member variables. There's no special place.

Core approach

    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe);
        }
    }

    public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }

await is the core method of CyclicBarrier, which relies on this method to realize the unified planning of threads, in which the internal implementation of doWait is called. Let's look at the following code:

    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        // As for the normal lock operation, as for the reason of using local variable operation,
        // You can read another article about ArrayBlockingQueue that I wrote
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            // Get Generation class
            final Generation g = generation;

            // Check whether the generation is broken. If it is broken,
            // That means that some threads may be interrupted or some unexpected state may lead to no solution
            // Complete the target of all threads arriving at the terminal (tripped) and only report errors
            if (g.broken)
                throw new BrokenBarrierException();

            // If the thread is interrupted externally, it needs to report an error, and internally it needs to
            // generation's token is set to true to enable other threads to sense interrupts
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            // Reduce the number of unfinished threads by 1
            int index = --count;
            // If the number of remaining threads is 0 at this time, all threads have completed, i.e. reached the tripped state
            if (index == 0) {
                boolean ranAction = false;
                try {
                    // If there is a preset method to execute after completion, execute
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    // At this time, since the thread of this cycle has been completed,
                    // So call the nextGeneration method to start a new cycle
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // If there are other threads unfinished at this time, the current thread turns on spin mode
            for (;;) {
                try {
                    if (!timed)
                        // If timed is false, trip blocks until it wakes up
                        trip.await();
                    else if (nanos > 0L)
                        // If timed is true, call awaitNanos to set the time
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        Thread.currentThread().interrupt();
                    }
                }

                // Check whether the generation is a token. If it is a token, throw an exception
                if (g.broken)
                    throw new BrokenBarrierException();

                // If G! = generation means generation
                // It has been given a new object, which means that either all threads have completed the task to start the next cycle,
                // Either it has failed, and then the next cycle is opened. In either case, return
                if (g != generation)
                    return index;

                // Force break if it has timed out
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

After reading this core code, we will go back to reflect on the significance of Generation. We can give the reasons for using Generation roughly:

Different from the implementation of CountDownLatch, CyclicBarrier takes a more complex way, because it involves inter thread interference and communication. CountDownLatch doesn't care about thread implementation and process. It's just a counter, while CyclicBarrier needs to know whether the thread ends normally and is interrupted. If it uses other ways, the cost will be compared Therefore, the author of CyclicBarrier shares the whole generation state among multiple threads through static internal classes, so as to ensure that each thread can get the state of the fence and better feedback its own state back. At the same time, this method is easy to reset, and also makes the CyclicBarrier reusable efficiently. As for why broken is not decorated with volatile, because all methods of the class are locked internally, there will be no problem of data asynchrony.

summary

CountDownLatch and CyclicBarrier may have some similarities in use, but after we read the source code, we will find that they are different in nature, implementation principle, implementation method and application scenario. In summary, they are as follows:

  1. CountDownLatch implementation relies directly on AQS; CyclicBarrier relies on ReentrantLock and Condition
  2. CountDownLatch exists as a counter, so it adopts a handy design. The source code structure is clear and simple, and the same function is relatively simple. In order to achieve multithreading control, CyclicBarrier adopts a more complex design, which also appears to be winding in code implementation.
  3. Because of the implementation of CyclicBarrier, compared with the one-time CountDownLatch, CyclicBarrier can be reused many times
  4. Different counting methods: CountDownLatch uses cumulative counting, while CyclicBarrier uses reciprocal counting

Posted by LAX on Thu, 23 Apr 2020 06:03:20 -0700