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