A detailed explanation of the multithreading concurrent tool class CyclicBarrier

Article directory

brief introduction

In the literal sense, CyclicBarrier means loopback barrier, which allows a group of threads to reach a state and then execute all at the same time. This is called loopback because it can be reused when all waiting threads have finished executing and the state of the CyclicBarrier has been reset. The reason why it is called barrier is that the thread will be blocked after calling the await method. This blocking point is called barrier point. After all threads call the await method, the threads will break through the barrier and continue to run downward.

CyclicBarrier is a synchronization aid that allows a group of threads to wait for each other until they reach a common barrier

Often used in a set of fixed number of threads must wait for each other

If the counter value is N, then the N-1 thread that calls the await method will be blocked because of reaching the barrier point. When the N thread calls await, the counter value is 0. At that time, the N thread will issue a notification to wake up the N-1 thread in front. That is, when all threads reach the barrier point, they can continue to execute downward together.

The thread enters the barrier through the await() method of the CyclicBarrier.

The CyclicBarrier instance is reusable: when all waiting threads are awakened, any thread executing CyclicBarrier.await() again will be suspended until the last thread of these threads executes CyclicBarrier.await()

Example

For example, create 10 new threads until all 10 threads call the await method, that is, after all the threads reach the barrier point, call the method defined during the initialization of the CyclicBarrier (call the Dragon)

public static void main(String[] args) throws InterruptedException {
		CyclicBarrier cyclicBarrier = new CyclicBarrier(10, () -> {
			System.out.println("Summon Dragon");
		});
		for (int i = 0; i < 10; i++) {
			new Thread(()->{
				try {
					System.out.println(Thread.currentThread().getName()+"Collect Dragon Balls");
					cyclicBarrier.await(); //Wait for other threads to complete their own operations. When the number of waiting threads reaches 10, the dragon will be called
				} catch (Exception e) {
					e.printStackTrace();
				}
			}, Thread.currentThread().getName()+":"+i).start();
		}

The following example: suppose a task is composed of phase 1, phase 2 and phase 3. Each thread should execute phase 1, phase 2 and phase 3 serially. When multiple threads execute the task, they must ensure that phase 1 of all threads is completed before entering phase 2, and phase 3 after all phases 2 of all threads are completed. This example makes use of the reusability of CyclicBarrier

 public static void main(String[] args) throws Exception {
    CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    for (int i = 0; i < 3; i++) {
        executorService.submit(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " step1");
                cyclicBarrier.await();
                System.out.println(Thread.currentThread().getName() + " step2");
                cyclicBarrier.await();
                System.out.println(Thread.currentThread().getName() + " step3");
            } catch (Exception e) {
            }
        });
    }

    executorService.shutdown();
}

//Output results:
pool-1-thread-1 step1
pool-1-thread-3 step1
pool-1-thread-2 step1
pool-1-thread-2 step2
pool-1-thread-1 step2
pool-1-thread-3 step2
pool-1-thread-3 step3
pool-1-thread-1 step3
pool-1-thread-2 step3

In the above code, each sub thread calls the await method after the execution of phase 1, and only when all threads reach the barrier point can they execute in one block, which ensures that all threads complete phase 1 before they start to execute phase 2. Then the await method is called after phase 2, which ensures that all threads can start the execution of phase 3 after completing phase 2. This function cannot be completed with a single CountDownLatch.

Realization principle

private static class Generation {
    boolean broken = false;
}


/** 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;

CyclicBarrier is based on exclusive lock, and its essence is based on AQS.

Parties are used to record the number of threads. This indicates how many threads call await before all threads break through the barrier and continue to run. At the beginning, count is equal to parties. Each time a thread calls the await method, it decrements by 1. When count is 0, it means that all threads have reached the barrier point.

You may wonder why the two variables of parties and count are maintained and only the
Don't forget that cycliecarrier can be reused. The reason for using two variables is that parties are always used to record the total number of threads. When the count counter value changes to 0, the value of parties will be assigned to count for reuse. These two variables are passed when the CyclicBarrier object is constructed as follows:

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

Another variable, barrierCommand, is also passed through the constructor. This is a task. The execution time of this task is when all threads reach the barrier point. Using lock first ensures the atomicity of the update counter count. In addition, the conditional variable trip of lock is used to support the synchronization of await and signal operations between threads.

Finally, in the variable generation, there is a variable broken, which is used to record whether the current barrier is broken. Note that the token here is not declared volatile, because variables are used within the lock, so there is no need to declare it.

private static class Generation {
    boolean broken = false;
}

Several important methods

  1. int await() method

When the current thread calls this method of CyclicBarrier, it will be blocked and will not return until one of the following conditions is met:

  • Each thread of the parties calls the await() method, that is, the thread has reached the barrier point;
  • If other threads call the interrupt() method of the current thread to interrupt the current thread, the current thread will throw an InterruptedException exception and return; -- when the token flag of the Generation object associated with the current barrier point is set to true, the broken barrierexception exception exception will be thrown and then return.

As can be seen from the following code, the dowait method is called internally. If the first parameter is false, the timeout is not set. At this time, the second parameter has no meaning.

public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}
  1. int dowait(boolean timed, long nanos) method

This method implements the core functions of CyclicBarrier, and its code is as follows:

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) If index==O, it means that all threads have reached the barrier point, and the task passed during initialization is executed
        int index = --count;
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                //(2) Perform task
                if (command != null)
                    command.run();
                ranAction = true;
                //(3) Activate other threads that are blocked by calling the await method and reset the CyclieBarrier
                nextGeneration();
                //Return
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // loop until tripped, broken, interrupted, or timed out
        //(4) If index is not 0
        for (;;) {
            try {
                //Timeout not set
                if (!timed)
                    trip.await();
                //Timeout set
                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();
    }
}

private void nextGeneration() {
    // signal completion of last generation
    //(7) Thread blocking in wake-up condition queue
    trip.signalAll();
    // set up next generation
    //Reset CyclicBarrier
    count = parties;
    generation = new Generation();
}

When a thread calls the dowait method, it first obtains the exclusive lock. If the parameter passed when creating the cyclebarier is 10, the next nine calling processes will be blocked. Then the thread that obtains the lock will decrement the counter count. After decrement, count=index=9. Because index!=O, the current thread will execute the code (4). If the current thread calls the wait() method without parameters, then timed=false, so the current thread will be put into the conditional blocking queue of the trip of the conditional variable, and the current thread will be suspended and the acquired lock lock will be released. If the await method with parameters is called, then timed=true, then the current thread will also be put into the condition queue of the condition variable and release the lock resource. The difference is that the current thread will be automatically activated after the specified time-out.

When the first thread to acquire the lock is blocked and releases the lock, one of the nine blocked threads will compete for the lock, and then perform the same operation as the first thread until the last thread acquires the lock. At this time, nine threads have been put into the conditional queue of conditional variable trip. Finally, count=index is equal to 0, so execute the code (2). If a task is passed when creating a CyclicBarrier, execute the task first before other threads are woken up, and then execute the code (3) after the task is finished. Wake up the other 9 threads and reset the CyclicBarrier, and then the 10 threads can continue to run downward.

Summary

The difference between cyclebarier and CountDownLatch is that the former can be reused, and the former is particularly suitable for the scenario of orderly execution of segmented tasks.

The bottom layer of the cyclebarier implements the atomic update of the counter through the exclusive lock ReentrantLock, and uses the conditional variable queue to realize the thread synchronization. A conditional variable trip is used to implement wait / notice in the CyclicBarrier. The concept of generation is used to indicate that the CyclicBarrier instance can be reused

Published 25 original articles, won praise 8, visited 952
Private letter follow

Tags: IE

Posted on Wed, 15 Jan 2020 07:38:23 -0500 by dhie