Java CyclicBarrier and source code explanation

Posted by fooDigi on Wed, 03 Nov 2021 18:20:57 +0100

What is a fence

In Java, the fence CyclicBarrier is a synchronization mechanism. The fence enables a group of threads to be blocked when they reach a synchronization point. Until the last thread in the group reaches the synchronization point, all blocked threads will be awakened and continue to execute, that is, the purpose is to continue to execute only when a group of threads all execute to the synchronization point, Otherwise, the thread that reaches the synchronization point first will block.
Fence CyclicBarrier and Lockout CountDownLatch After analyzing the source code, we will briefly list the differences between the two.

Realization idea of fence

The constructor of CyclicBarrier class will require the user to assign an initial value to the parties variable. The parties variable represents the total number of threads in a group, and the counter variable count in CyclicBarrier represents the number of threads that have not reached the synchronization point. When a thread reaches the synchronization point, the await() method will be called. In this process, the count will decrease automatically. At this time, if the counter value is non-zero, it indicates that there are still threads that have not reached the synchronization point, and the current thread enters the blocking state. If the counter value is zero, it indicates that all threads have reached the synchronization point, and all threads will be awakened to continue execution.

Fence source code analysis

Before analyzing the core methods, first pay attention to the member variables and construction methods in the CyclicBarrier class. The source code is as follows:

	
	//Reentry lock is mainly used to ensure thread safety
    private final ReentrantLock lock = new ReentrantLock();
    //Condition is used to block and wake up threads
    private final Condition trip = lock.newCondition();
	//Represents the total number of threads in a group
    private final int parties;
	//This variable represents the operation to be performed before all threads reach the synchronization point and wake up (null able indicates no operation)
    private final Runnable barrierCommand;
	
    private Generation generation = new Generation();
	//Indicates the number of threads that have not yet executed to the synchronization point. When the variable is 0, it indicates that all threads have executed to the synchronization point
	private int count;
	
	// It is mainly used for initialization. The description of the barrierAction parameter is the same as that of the variable barrierCommand
	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);
    }

Next, focus on the static internal class generation and member variable generation in the CyclicBarrier class. In order to explain the convenience of generation, we must first understand that it is related to locking and Semaphore Different, CyclicBarrier is reusable. The specific implementation will be mentioned later. First, let's explain the Generation:

	/**
	 * When it comes to Generation generation, it's easy to think of garbage collection Generation, but I think the two meanings are irrelevant
	 * The author thinks that the Generation here is more like the unique identification of CyclicBarrier
	 * The boolean variable broken is used to indicate whether the current generation has been destroyed
	 * If the current thread judges that the current generation is not damaged, it will execute normally
	 * If the judgment has been broken, the execution should not continue, and all threads in the waiting queue should be awakened 
	 * In the following cases, broken will be set to true, indicating that the current generation has been destroyed
	 * 1.Waiting to join the waiting queue or a thread in the waiting queue is interrupted
	 * 2.barrierCommand Exception thrown during execution
	 * 3.Blocking thread waiting time exceeds the set threshold
	 * The following source code analysis will present the above situation
	 * 
	 */
	private static class Generation {
        boolean broken = false;
    }

	private Generation generation = new Generation();

The following focuses on the core code of CyclicBarrier. The source code is as follows:

	/**
	* A thread that calls the await() method indicates that the thread has reached the synchronization point
	* CyclicBarrier Two versions of the await() method are provided
	* Whether the time threshold is set or not, the dowait() method is eventually called, and the core logic is in this method
	*/
	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));
    }
    
	//The core method of CyclicBarrier
	private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        /**
        * First, the thread attempts to acquire an exclusive lock
        * This ensures that multiple threads will not modify variables such as count and generation at the same time, ensuring thread safety
        */
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//Get current generation
            final Generation g = generation;
			//Throw an exception if the current generation has been destroyed
            if (g.broken)
                throw new BrokenBarrierException();
			/**
			* Corresponding to case 1 in the Generation annotation, if the thread to be added to the waiting queue is interrupted, it is determined that the current Generation is damaged
			* Here, breakBarrier() will set the current generation's broken to true and wake up other threads at the same time
			* This way, if other awakened threads execute the await() method again
			* It will stop at the previous judgment and throw an exception
			*/
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
			/**
			* index Used to indicate the number of threads remaining that have not reached the synchronization point
			* As for why the count variable is not directly used here, I guess it is based on thread safety considerations
			* Looking at the dowait method 
			* Although only one thread can hold the lock at the same time, there is no write operation to count except self subtraction in the dowait method
			* However, there is still a non private method to reset the fence. reset() can modify the value of count concurrently, causing thread safety problems
			*/
            int index = --count;
            //An index of 0 indicates that all threads execute to the synchronization point
            if (index == 0) {
            	//It is used to judge whether the code in the following try blocks (specifically barrierCommand) is executed successfully
                boolean ranAction = false;
                try {
                	//Execute barrierCommand preferentially if specified before waking up other threads
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    //Successful execution
                    ranAction = true;
                    //Generation operation 
                    //This method wakes up the thread in the waiting queue, creates a new Generation instance and resets the count
                    nextGeneration();
                    return 0;
                } finally {
                	/*
                	* If ranAction is false, an exception occurs during the execution of barrierCommand
                	* Corresponds to case 2 in the Generation comment
                	*/
                	
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // If it can be executed here, it means that there are still threads that have not reached the synchronization point
            for (;;) {
                try {
                	/**
                	* Call different versions of await methods according to whether the time threshold is set
                	* await Method is to add the current thread to the waiting queue (blocking)
					*/
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                	/**
                	* If an interrupt exception is caught, it still corresponds to case 1 in the Generation annotation. You need to set the broken representation to true
                	*/
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                    	//If the if condition is not met, the thread does not belong to the current generation interrupt                    	
                        Thread.currentThread().interrupt();
                    }
                }
				//It is detected that the current generation has been corrupted. An exception is thrown
                if (g.broken)
                    throw new BrokenBarrierException();
				//During the execution of dowait, nextGeneration is executed or reset has been called by the outside world, which has been returned by the generation method
                if (g != generation)
                    return index;
				/**
				* The wait timeout corresponds to case 3 in the Generation annotation. Set the broken to true
				*/
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
        	//Regardless of the execution result, the reentry lock is released so that other threads in the synchronization queue try to acquire the lock
            lock.unlock();
        }
    }
	

Finally, other methods are briefly analyzed. The source code is as follows:

	/**
	* Replacement operation if the methods annotated in this method also appear in other methods, they will not be annotated later
	* Lock acquisition is not attempted here because the method is private and will only be called thread safe in the method dowait attempting to acquire the lock
	*/
	private void nextGeneration() {
        // Wake up all threads in the waiting queue
        trip.signalAll();
        // Reset count counter
        count = parties;
        //Create a new Generation instance to represent the Generation
        generation = new Generation();
    }
	//No attempt to acquire lock. Same as nextGeneration()
	private void breakBarrier() {
		//Mark current generation is corrupted
        generation.broken = true;
        count = parties;
        trip.signalAll();
    }
	//The atomic operation of getting the number of bus passes does not need to obtain a lock
	public int getParties() {
        return parties;
    }
	//Exclusively judge whether the current generation has been damaged, and ensure that other threads will not write to the broken at the same time
	public boolean isBroken() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return generation.broken;
        } finally {
            lock.unlock();
        }
    }
    //Reset CyclicBarrier exclusively
	public void reset() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            breakBarrier();   // break the current generation
            nextGeneration(); // start a new generation
        } finally {
            lock.unlock();
        }
    }
	
	//Exclusively obtain the number of waiting threads to ensure that other threads will not write to count at the same time
	 public int getNumberWaiting() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return parties - count;
        } finally {
            lock.unlock();
        }
    }

Simple example of fence use

Suppose there are the following scenario requirements: suppose N groups of data need to be calculated separately, make the calculation results of each group visible to a thread, and then the thread obtains the final result after some calculations, and the process needs to be executed multiple times. You can consider using a fence to solve this problem. The example code and results are as follows

	public class CyclicBarrierTest {

    public static final int N = 3;

    private static class Compute implements Runnable{

        private final CyclicBarrier barrier;
        private final Integer id;

        private Compute(CyclicBarrier barrier, Integer id) {
            this.barrier = barrier;
            this.id = id;
        }

        @Override
        public void run() {
            System.out.println("Start the second step" + id + "Group data calculation");
            try {
                //Some calculation processes
                Thread.sleep(500);
                System.out.println("The first" + id + "Group data calculation completed.Visible result of main thread");
                barrier.await();
                System.out.println("whole" + id + "Blocking complete");
            } catch (Exception e) {}
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(N+1);
        System.out.println("First round calculation");
        for (int i = 0; i < N; i++) {
            Thread thread = new Thread(new Compute(barrier,i));
            thread.start();
        }
        try {
            System.out.println("The main thread is blocked waiting for the calculation results of other threads");
            barrier.await();
            Thread.sleep(50);
            System.out.println("The main thread gets three groups of calculation results and gets the final result");
        } catch (Exception e) {}
        barrier.reset();
        //Just create one thread to prove reusable
        Thread.sleep(50);
        System.out.println("Second round calculation");
        Thread thread = new Thread(new Compute(barrier,1));
        thread.start();

    }

}
First round calculation
 The main thread is blocked waiting for the calculation results of other threads
 Start group 1 data calculation
 Start calculation of group 0 data
 Start calculation of group 2 data
 Group 0 data calculation completed.Visible result of main thread
 Group 1 data calculation completed.Visible result of main thread
 The calculation of group 2 data is completed.Visible result of main thread
 All 2 blocked
 All 0 blocked
 All 1 blocked
 The main thread gets three groups of calculation results and gets the final result
 Second round calculation
 Start group 1 data calculation
 Group 1 data calculation completed.Visible result of main thread

Some characteristics of the fence are different from those of CountDownLatch

  • Fence is used to wait for a group of threads to execute to the synchronization point, while locking is used for one or more threads to wait for the execution of an external event.
  • The fence is reusable and the locking is disposable.
  • The fence uses the synchronization queue (obtaining and releasing locks) and the waiting queue (blocking and waking threads), and the locking only uses the synchronization queue.

The above is the whole content of this article
The author has little talent and learning. If there are mistakes in the article, I hope to correct them

Topics: Java Concurrent Programming JUC