CountDownLatch, CyclicBarrier and Semaphore can be used for many interesting implementations in our normal development, such as running, allowing multiple threads to start together, or building a house, allowing multiple threads to check in after the creation, or like a Ferris wheel, with 20 seats. I prefer that only one person can go in at a time, Generally speaking, the functions of these three methods are very similar. They manage the cooperation of multiple threads, but they are different in actual use. After further understanding the three tool classes from the source code, I wrote this article for future reference
1, CountDownLatch
He corresponds to the example of the house mentioned above. He can let one or more threads wait until the execution of other threads or thread groups is completed. Let's take building a house as an example to write an example code
public class BuildHouse(){ public static void main(String[] args) { CountDownLatch countDown = new CountDownLatch(3); System.out.println("Start building a house"); Thread builder1 = new Thread(new Builder(countDown, "Builder 1")); Thread builder2 = new Thread(new Builder(countDown, "Builder 2")); Thread builder3 = new Thread(new Builder(countDown, "Builder 3")); builder1.start(); builder2.start(); builder3.start(); try { countDown.await(); System.out.println("When the house is built, stay happily"); } catch (InterruptedException e) { e.printStackTrace(); } } static public class Builder implements Runnable{ private CountDownLatch countDownLatch; private String threadName; public Builder(CountDownLatch countDown, String name) { countDownLatch = countDown; threadName = name; } @Override public void run() { try { System.out.println(threadName+"start-up"); Thread.sleep(1000L); System.out.println(threadName+"finish the work"); } catch (InterruptedException e) { System.out.println(threadName +"exception occurred"); }finally { countDownLatch.countDown(); } } } }
In the code, we have three workers to build a house together. We can't move in until all the workers have finished their work. Here, countDownLatch can make the main thread wait when the three sub threads haven't finished their work. Once they have finished their work, they can continue to execute downward. Now that you understand the usage, you can begin to explore how it implements the feature
Step 1: new CountDownLatch(3)
public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); }
In the constructor, we create a Sync object and pass in our construction parameters. Like ReentrantLock, Sync here is an internal class that implements AQS. Here, we mainly set our count to the AQS state as a public resource.
Step 2: countDown.await()
//CountDownLatch#await() public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); } //AQS#acquireSharedInterruptibly public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); } //CountDownLatch#tryAcquireShared() protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }
The await method is mainly to call AQS's method of obtaining shared locks. This method is a template method provided by AQS. We don't go into depth here. We mainly need to understand the tryAcquireShared() custom implemented by CountDownLatch. The logic is very simple, that is, judge whether the state is 0. If it is 0, it means that obtaining locks is normal, Otherwise, thread waiting will be hung in acquireSharedInterruptibly, that is, when the await method is invoked in the example code, because state is 3 of the initialized object we initialized, so it is not 0, when the main thread is suspended.
Step 3: countDown.countDown()
//CountDownLatch#countDown() public void countDown() { sync.releaseShared(1); } //AQS#releaseShared public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } //CountDownLatch#tryReleaseShared() protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } }
The countDown method is also a template method to call AQS to release the shared lock first. Here we mainly see the tryrereleaseshared method implemented by CountDownLatch. The logic is also very simple. It is to subtract 1 when the state is not 0. Because it is a multi-threaded operation, CAS is used to update the state field. If the state is 0, it returns true, At this time, releaseShared will continue to call doreaseshared() to wake up the suspended thread. The corresponding example code is: when each worker completes his work, he will call countDown to reduce the state by 1. When the third worker reduces the state by 1, tryrereleaseshared will return to true, and then call doreaseshared() to wake up the main thread, Complete the whole business process.
Process summary
The whole process is complex. When we create CountDownLatch, we will set the values we specified for the shared state field. Then we call the await method to hang up the thread according to whether the state is greater than 0. The other waiting threads call the countDown method to subtract the state until state is equal to 0, then wake up the thread being hung, and complete the thread control logic. The general flow chart is as follows
CountDownLatch notes
- If the child thread exits abnormally before calling countDown, the condition of state==0 will never be tenable, and the waiting thread will wait indefinitely. Here, we can use the await(long timeout, TimeUnit unit) method with timeout to wake up automatically after waiting for the specified time.
- It corresponds to the first question, calling await() method in finally as much as possible.
- CountDownLatch cannot be reused because the value of state is directly subtracted in the code. If it is reused, the purpose of suspending threads cannot be achieved because the state has not been 0, which is in contrast to the CyclicBarrier to be introduced later.
2, CyclicBarrier
CyclicBarrier is mainly used to make a group of threads wait for each other until all threads reach a synchronization point and then continue to execute together. This scenario corresponds to running. We can understand it from the name of barrier (fence). Barrier is like the starting line. All athletes can't start until they reach the starting line before and after the referee fires a gun. Take this as an example, A piece of code. public class CyclicBarrierDemo {
public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(3); Thread runner1 = new Thread(new Runner(cyclicBarrier, "Zhang San")); Thread runner2 = new Thread(new Runner(cyclicBarrier, "Li Si")); Thread runner3 = new Thread(new Runner(cyclicBarrier, "Wang Wu")); runner1.start(); runner2.start(); runner3.start(); } static class Runner implements Runnable{ private CyclicBarrier cyclicBarrier; private String runnerName; public Runner(CyclicBarrier cyclicBarrier, String runnerName) { this.cyclicBarrier = cyclicBarrier; this.runnerName = runnerName; } @Override public void run() { try { System.out.println(runnerName+"Reach the starting line"); cyclicBarrier.await(); System.out.println(runnerName+"Run"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } } }
In this example, there are three athletes, but their preparation time is different, but they all need to wait until all athletes reach the starting line before they can start running together. Like CountDownLatch, let's analyze each method used
Step 1: new CyclicBarrier()
//CyclicBarrier#CyclicBarrier public CyclicBarrier(int parties) { this(parties, null); } //CyclicBarrier#CyclicBarrier public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; this.barrierCommand = barrierAction; } //CyclicBarrier#CyclicBarrier public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); //The specified number of threads is backed up to support reuse this.parties = parties; //Number of threads specified this.count = parties; //Method to execute when all threads are ready (not required) this.barrierCommand = barrierAction; }
The process of creating a new one is mainly to set the number of threads. Unlike CountDownLatch, CyclicBarrier does not have the ability to directly inherit AQS. Instead, it implements the whole function by using ReentrantLock. Here, the count field is used to save the number of threads, and a parties field is also set to copy the number of threads to support reuse.
Step 2: cyclicBarrier.await()
//CyclicBarrier#await public int await() throws InterruptedException, BrokenBarrierException { try { return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen } } //Only the main logic CyclicBarrier#dowait() is retained private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException { //Use exclusive locks to lock resources final ReentrantLock lock = this.lock; lock.lock(); try { //Generation is a marker class. Because the cyclicBarrier can be reused, it is necessary to use generation to mark the current round, just like running. Each time 10 athletes run, generation will represent the competition of these 10 athletes. When it comes to the next round, a new generation will represent the competition of 10 athletes in the next round. Athletes can Judge whether your current game is over by whether the generation object has been changed or by the broken in it final Generation g = generation; if (g.broken) throw new BrokenBarrierException(); //If one thread interrupts unexpectedly, all other threads wake up and interrupt to exit //It's like a sports athlete can't participate in the game because of injury, so cancel the game and others quit unconditionally if (Thread.interrupted()) { breakBarrier(); throw new InterruptedException(); } int index = --count; if (index == 0) { // tripped boolean ranAction = false; try { //The logic to be executed after all threads arrive, which is specified by CyclicBarrier(int parties, Runnable barrierAction) final Runnable command = barrierCommand; if (command != null) command.run(); ranAction = true; nextGeneration(); return 0; } finally { if (!ranAction) breakBarrier(); } } // loop until tripped, broken, interrupted, or timed out for (;;) { try { //timed refers to the waiting time, which can be specified through await (long timeout, timeunit). When it exceeds the time, it will wake up automatically, and a timeout exception will be thrown later to interrupt all other threads if (!timed) trip.await(); 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(); } }
Some details in the code have been annotated in the code. Here we mainly explain the whole implementation logic. After calling the await method, the child thread will obtain the lock, then subtract 1 from the set count and judge whether it is 0 after subtracting 1. If it is 0, it means that all threads have completed this step and they are the last, Then you can wake up all waiting threads. Otherwise, you can suspend yourself on the trip condition queue and wait for the wake-up of the last arriving thread. Corresponding to running, when the athletes reach the starting point, they will go to see whether other athletes have arrived. If they have arrived, they will tell you to run together. If not, they will wait for the unreachable athletes in situ.
Process summary
The process here is not complicated. It is mainly to obtain the lock through ReentrantLock, and then judge whether all threads have arrived. If not, hang on the corresponding condition queue and wait for wake-up. The flow chart is as follows
cyclicBarrier precautions
1. During the waiting process, if a thread actively exits or abnormally interrupts, other threads will interrupt the waiting and throw an exception, which will not be executed normally. Therefore, the business code needs to catch the exception and make a thorough logic. 2. In order to prevent the thread from waiting indefinitely, the waiting time limit can be passed in when calling the wait method. If the execution is not resumed normally after the expiration, a timeOut exception will be thrown, It needs to be captured in the business code and processed subsequently. 3. If it is necessary to execute the specified logic when all threads wake up again, for example, after all athletes reach the starting point, they do not start directly, and wait for the referee to launch a signal gun first, then you can use the overload method to pass in the logic of transmitting the signal gun when creating the cyclicBarrier, This corresponds to the execution logic at the barrierCommand in dowait.
1, Semaphore
Semaphore semaphore is used to control the number of threads that resources can be accessed at the same time. It can generally be used for traffic control. Corresponding to the example of the ferris wheel we mentioned earlier, the ferris wheel has only 20 positions in total, but there must be more than 20 people waiting in line. Semaphore is equivalent to the management personnel at the entrance to control that there are only 20 people on it all the time.
public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(2); Thread passenger1 = new Thread(new Passenger(semaphore, "Passenger 1")); Thread passenger2 = new Thread(new Passenger(semaphore, "Passenger 2")); Thread passenger3 = new Thread(new Passenger(semaphore, "Passenger 3")); Thread passenger4 = new Thread(new Passenger(semaphore, "Passenger 4")); passenger1.start(); passenger2.start(); passenger3.start(); passenger4.start(); } static class Passenger implements Runnable{ private Semaphore semaphore; private String name; public Passenger(Semaphore semaphore, String name) { this.semaphore = semaphore; this.name = name; } @Override public void run() { try { System.out.println(name+"In line, take the ferris wheel"); semaphore.acquire(); //Thread.sleep(2000L); System.out.println(name+"Got on the ferris wheel"); } catch (InterruptedException e) { e.printStackTrace(); }finally { semaphore.release(); System.out.println(name+"Off the ferris wheel"); } } } }
In this example code, suppose that there are only two positions on the ferris wheel, but there are four passengers who want to sit. When each passenger tries to enter the ride, he needs to ask the waiter whether there is a vacant seat to enter, that is, the acquire method to obtain resources, enter if there is one, and wait in line if there is none. The passengers who enter and sit also need to tell the waiter when they get down, I came down. There is a vacancy on it, that is, the release method, to release resources. Let's further explore the implementation details of Semaphore through these two methods
Step 1: new semaphore()
//semaphore#Semaphore public Semaphore(int permits) { sync = new NonfairSync(permits); } //semaphore.NonfairSync NonfairSync(int permits) { super(permits); } //semaphore.Sync Sync(int permits) { setState(permits); } //AbstractQueuedSynchronizer#setState protected final void setState(int newState) { state = newState; }
The whole construction process is from top to bottom. Finally, the shared variable state of AQS is set to the number we specify. It can be seen that semaphore creates an internal class Sync that inherits AQS and relies on the ability of AQS to realize the concurrency function
Step 2: semaphore.acquire();
//semaphore#acquire public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } //AbstractQueuedSynchronizer#acquireSharedInterruptibly public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); } //semaphore#tryAcquireShared here takes the fair implementation method. The default is unfair protected int tryAcquireShared(int acquires) { for (;;) { //Compared with unfairness, there is only more judgment logic if (hasQueuedPredecessors()) return -1; int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
The logic here is also very simple. First, call the AQS method of obtaining shared locks, and then call the logic of obtaining locks implemented by semaphore in this method. If the lock is not obtained, AQS provides a thread queue to wait. Here we mainly look at semaphore's implementation of obtaining resources and enter tryacquishared, First, it will judge whether there is a thread waiting in front, and if so, it will return - 1 to start queuing (there is no logic for non fair locks, and the subsequent resource acquisition operation will be carried out directly). Otherwise, it will try to obtain resources and update the state. Here, in order to ensure the atomicity of judgment and update, cas is used to update the state value, and try again after the update fails. The corresponding example is, When a person comes to make a Ferris wheel and finds someone waiting in line, he will honestly queue up. If not, he will ask the administrator whether there is a vacant seat, go up if there is one, and queue up if there is no one. It is not fair, that is, whether there is a queue or not, he will directly ask the administrator if there is a position, go up if there is one (whatever the people behind scold), and no longer queue up
Step 3: semaphore.release();
//semaphore#release public void release() { sync.releaseShared(1); } //AbstractQueuedSynchronizer#releaseShared public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } //semaphore#releaseShared protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); if (compareAndSetState(current, next)) return true; } }
The same way to release resources is to call the AQS method first. After releasing resources, the AQS will help us wake up the threads in the queue. We mainly subtract 1 from the state when obtaining resources, and then add 1 to the state when releasing. After releasing, the AQS will help us wake up the threads in the queue. That is, when one person is down, the administrator will let another person up, This man is the one at the top
Process summary
Go straight to the flowchart. There's nothing to say
Attention
1. semaphore has two modes: fair and unfair. The default mode is unfair. The only difference is that in the fair mode, tryAcquireShared will judge whether a node is waiting, and if it is, it will be directly queued to the end of the waiting queue. If it is not fair, it will not have this judgment. Because the unfair mode can reduce some wake-up operations, its performance is better than that of the fair mode, However, the unfair mode may make the threads in the waiting queue never get resources. The two modes have their own advantages and disadvantages, so it is necessary to determine which mode to use according to the business scenario.
summary
These three methods seem to coordinate the execution of multiple threads, but the actual application scenarios are very different. CountDownLatch is to wait for multiple threads to complete at the same time before subsequent operations. CyclicBarrier enables multiple threads to start execution together. Semaphore ensures that there are only a specified number of threads in parallel at the same time, In fact, it's easy to understand the correspondence between them and several examples in this article. In the analysis process, you will always see the abstract class AbstractQueuedSynchronizer, which is the basis of the whole juc package. If you can clarify this class, these concurrency tool classes under locks will be well understood.