CountDownLatch shared lock source code analysis

CountDownLatch does not explain the usage scenarios of CountDownLatch, you can read other articles. Only the source code is analyzed here.

await method

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

Directly call the acquireSharedInterruptibly method of AbstractQueuedSynchronizer

  public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

Acquire in shared mode, and abort if interrupted. By first checking the interrupt status, then calling tryAcquireShared at least once and returning successfully. Otherwise, the thread will be queued, and may repeatedly block and unblock, and call tryAcquireShared until it succeeds or the thread is interrupted 

   protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

 When the state value in CountDownLatch is 0, the blocking is unblocked, and all other conditions are blocked. Therefore, the tryAcquireShared method of CountDownLatch will determine whether state is equal to 0. If it is equal to 0, it will not block, and it will return 1. If it is not equal to 0 (greater than 0), it will return -1, and it will enter the blocking queue until it is unblocked.

 
   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);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

1. This is different from the exclusive lock call addWaiter, the parameter here is Node.SHARED (shared mode)

2. If the front node is head, try to acquire the lock, r>=0 means state==0, no need to block,

3. If the front node is not head, or r<0, block the current thread and set the front node to -1 (SIGNAL). Waiting to be awakened

4. Regardless of interruption, after being awakened, follow the for loop to execute step 2. If the acquisition still fails, continue to block.

countDown method

Decrease the count of the latch, and if the count reaches zero, release all waiting threads. If the current count is greater than zero, it is decremented. If the new count is zero, all waiting threads will be re-enabled for thread scheduling. If the current count is equal to zero, then nothing will happen

 public void countDown() {
        sync.releaseShared(1);
    }

 Directly call the releaseShared method of AbstractQueuedSynchronizer

   public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

If the attempt to release the shared lock is successful, that is, the state is reduced to 0, then

The tryReleaseShared method overridden by CountDownLatch

     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;
            }
        }

1. If the current state is 0, return false and do nothing

2. If it is not 0, decrease state by 1, and return whether it is decreased to 0.

AbstractQueuedSynchronizer#doReleaseShared方法:

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

1. If the head node is not empty and is not equal to the tail node, it means that there is a blocked node. When adding a blocking node, the waitStatus of its predecessor node will be set to -1. Set the head node waitStatus to 0 through cas. If the setting fails, it means that the previous waitStatus value has changed. If the setting is successful, the first node in the queue will be woken up.

After the first node in the queue is awakened:

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

It can be seen that the awakened node will set itself as the head node, and then call doReleaseShared to wake up the next node. In this way, these two methods are called cyclically, and all threads will eventually be released.

Then talk about the above doReleaseShared method to discuss why cas will fail if the head node waitStatus is set to 0. Because the thread that wakes up the next node and the current thread are executed concurrently. Here suppose that the current thread t0 successfully sets the head node waitStatus to 0 for the first time in the current thread t0, then wakes up the thread t1, after t1 is awakened, sets its own node to head, t0 executes to h==head, which is false, because it is replaced by t1 , So cycle again. This method will also be called by t1, which is the node whose head is t1. If both t0 and t1 are executed until the waitStatus of the head is set to 0, then only one thread succeeds.

2. ws==0, it means it is the last node, because the value of ws is set by its successor node. If cas fails, it means that a new node has joined in, and ws is set to -1, then re-for to wake up the newly added node. If it succeeds,

3. If h==head is true, it means that the awakened thread has not set itself as the head node head, and the awakening thread triggers the line of code and exits the loop. Although the awakened thread exits, the subsequently awakened thread will still execute the doReleaseShared method, and the subsequent thread will still be awakened.

to sum up

Simply put, when a thread calls await, if the state value is not 0, the thread is blocked. If countDown reduces the state to 0, the thread that is reduced to 0 is responsible for waking up the first node of the blocking queue. After the node is woken up, it will wake up its successor nodes, and so on, all blocked threads are woken up .

Guess you like

Origin blog.csdn.net/sinat_33472737/article/details/114969159