Combine CountDownLatch and CyclicBarrier to understand the shared lock part of AQS
1. Use of CountDownLatch
Let's take a look at how CountDownLatch is used
public class CountDownLatchTest { public void CountDownLatchTest() throws InterruptedException { CountDownLatch doneSignal = new CountDownLatch(5); ExecutorService e = Executors.newFixedThreadPool(8); // Create N tasks and submit them to the thread pool for execution for (int i = 1; i <= 5; ++i) // create and start threads e.execute(new WorkerRunnable(doneSignal, i)); // Wait for all tasks to be completed before this method returns. It is blocked here before it is completed doneSignal.await(); // wait for all to finish System.out.println("The tasks of all threads are finished"); e.shutdown(); System.out.println("Thread pool closed"); } class WorkerRunnable implements Runnable { private final CountDownLatch doneSignal; private final int i; WorkerRunnable(CountDownLatch doneSignal, int i) { this.doneSignal = doneSignal; this.i = i; } public void run() { doWork(i); // When the task of this thread is completed, call the countDown method doneSignal.countDown(); } void doWork(int i) { System.out.println("thread " + i + "Your task is done"); } } public static void main(String[] args) throws InterruptedException { new CountDownLatchTest().CountDownLatchTest(); } }
Output results
The task of thread 1 is finished The task of thread 2 is finished The task of thread 3 is finished The tasks of all threads are finished The task of thread 4 is finished The task of thread 5 is finished Thread pool closed
From the output results, we can see that only five threads have completed the task await() the method will return, and then execute the following. It will be blocked before it is completed. It will not be blocked until five threads complete the task. The practical application is to divide a large task into several small tasks, and then let different threads execute concurrently. For example, divide a piece of data into multiple segments, process a part of each data, and return until the whole data is processed
2. CountDownLatch principle (shared lock)
After using CountDownLatch, you can start the principle of CountDownLatch. Let's see how CountDownLatch uses AQS shared locks
2.1 construction method
The constructor is used to initialize the value of state, which is equivalent to how many locks have been initialized. Negative numbers are not allowed
public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); } Sync(int count) { setState(count); }
2.2 await () method
Suppose that thread 1 and thread 2 now call the await () method
The first call is the await () method of CountDownLatch
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
Then call the acquireSharedInterruptibly () method of AQS.
//Because this method will handle interrupts, judge the interrupt status first public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); //state is not set up for 0 conditions and then calls doAcquireSharedInterruptibly(arg). if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
Then call the tryAcquireShared () method of the static internal class Sync of CountDownLatch.
protected int tryAcquireShared(int acquires) { //1 is returned only when state is 0 return (getState() == 0) ? 1 : -1; }
Tryacquisureshared returns - 1, so the doacquisuresharedinterruptible () method of AQS is called
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { //Queue thread 1. Since the queue is empty before thread 1 is queued, thread 1 will create a head node, and then queue thread 1 //Because it is a shared lock and no thread holds the lock alone, the exclusiveOwnerThread variable is not used final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { //Get precursor node final Node p = node.predecessor(); //The precursor node is the head node if (p == head) { //You can try to get the lock int r = tryAcquireShared(arg); //Greater than or equal to 0 indicates success if (r >= 0) { //Set the current node as the new head node setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } //If the lock acquisition fails, suspend and set the ws of the precursor node to - 1 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //Throw an exception when interrupted, and will not continue to try to grab the lock throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
If thread 1 and thread 2 fail to call the await () method to obtain the lock, they will be suspended first and wait to be awakened
The queue at this time is
2.3. countDown() method
Every time the countDown() method is called, the state will be reduced by 1, which is equivalent to unlocking a lock until the state is reduced to 0, which means that the lock has been unlocked
Thread 3 and thread 4 call the countDown() method of CountDownLatch respectively
public void countDown() { sync.releaseShared(1); }
Then call the releaseShared () method of AQS.
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
Then call the tryReleaseShared() method of the CountDownLatch static internal class.
protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { //Get current status value int c = getState(); //0 means no unlocking if (c == 0) return false; int nextc = c-1; //cas subtracts state by 1 if (compareAndSetState(c, nextc)) //When the state is 0, it returns true return nextc == 0; } }
If the tryrereleaseshared() method returns true, it means that all locks have been unlocked. You can call the doReleaseShared() method of AQS to wake up all blocked threads
//This method is called only when the value of state is 0 or interrupted private void doReleaseShared() { for (;;) { Node h = head;//Head node //The header node is not empty, and the queue has more than one node if (h != null && h != tail) { int ws = h.waitStatus; //Header node ws //If the value of the head node ws is SIGNAL-1, it means that the successor node of the head node will be awakened if (ws == Node.SIGNAL) { //Set the ws of the head node to 0 through cas. cas is used because multiple threads may modify ws if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases //Wake up the successor node of the head node unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } //Until the nodes behind the head are awakened, only the head node in the queue will not continue to wake up if (h == head) // loop if head changed break; } }
At this time, the successor node of the head node is awakened, that is, thread 1 is awakened
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); //After being awakened, r is 1, that is, greater than or equal to, so setHeadAndPropagate(node, r) will be called if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } //After being awakened, the interrupt will be checked first, and then the cycle will continue if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below //Set thread 1 as the new head node setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; //It will continue to wake up the successor node of the new head node, thread 2, until all threads are awakened if (s == null || s.isShared()) doReleaseShared(); } }
summary
1. CountDownLatch uses the AQS shared lock, which means that multiple threads can lock and unlock, while an exclusive lock can only be locked and unlocked by one thread
2. A thread calls the await () method. If the state is not 0, it will enter the synchronization queue and wait for wake-up to obtain the lock
3. After other threads call the countDown () method, if the state is 0, the nodes behind the head in the synchronization queue will wake up
4. The same thread cannot call the await () method and the countDown () method at the same time because it cannot wake itself up