77 the Application Principles of Aqs in Count Down Latch, Etc

77 The Application Principles of AQS in CountDownLatch, etc #

In this lesson, we will mainly explain the application principles of AQS in the CountDownLatch class, that is, how to use AQS to implement the thread cooperation logic of CountDownLatch itself. This lesson will include some source code analysis.

Usage of AQS #

First, let’s talk about the usage of AQS. If you want to use AQS to write your own thread cooperation utility class, usually you can divide it into the following three steps, which are also the main steps of using the AQS class in the JDK:

  • The first step is to create your own thread cooperation utility class and write a Sync class inside it, which inherits AbstractQueuedSynchronizer, that is, AQS;
  • The second step is to design the thread cooperation logic of your utility class. In the Sync class, override the corresponding methods based on whether it is exclusive. If it is exclusive, override methods such as tryAcquire and tryRelease. If it is non-exclusive, override methods such as tryAcquireShared and tryReleaseShared;
  • The third step is to implement the relevant acquire/release methods in your own thread cooperation utility class and call the corresponding methods of AQS inside them. If it is exclusive, call methods like acquire or release. If it is non-exclusive, call methods like acquireShared, releaseShared, or acquireSharedInterruptibly.

Through these three steps, you can make use of AQS. Since these three steps have been condensed and refined, you may find them difficult to understand at the moment. However, we will have specific examples later to help you understand. For now, just have a preliminary impression.

You may have noticed that in the second step mentioned above, we override specific methods based on certain conditions. This approach seems to be rarely seen before, or you may think, is there a better way? For example, by implementing an interface, you will naturally know which methods need to be overridden. Why do you have to inherit a class first and then determine which methods to override? Isn’t this setting obstacles for yourself?

The answer to this question is actually explained in a paper by Doug Lea, the original author of AQS. He believes that if you implement an interface, then every abstract method needs to be implemented. For example, if you treat AQS as an interface, there are many methods that need to be implemented, including tryAcquire, tryRelease, tryAcquireShared, tryReleaseShared, and so on. But in fact, we don’t need to override every method. Depending on the requirements, implementing some of them selectively is enough. Therefore, it is designed not to use interface implementation, but to use class inheritance and method overriding.

You may have another question. After inheriting a class, it is not mandatory to override methods. So what if we don’t override any method? The answer is, if we don’t override the tryAcquire and other methods mentioned earlier, it won’t work because an exception will be thrown when executed. Let’s take a look at the default implementation of these methods in AQS.

There are four methods below: tryAcquire, tryRelease, tryAcquireShared, and tryReleaseShared:

protected boolean tryAcquire(int arg) {

    throw new UnsupportedOperationException();

}

protected boolean tryRelease(int arg) {

    throw new UnsupportedOperationException();

}

protected int tryAcquireShared(int arg) {

    throw new UnsupportedOperationException();

}

protected boolean tryReleaseShared(int arg) {

    throw new UnsupportedOperationException();

}

As you can see, there is only one line of implementation code inside each of them, which is to throw an exception directly. Therefore, after inheriting AQS, we are required to override the relevant methods so that our thread cooperation class can run normally.

Application of AQS in CountDownLatch #

Above, we discussed the basic process of using AQS. Now, let’s use an example to help understand the application of AQS in CountDownLatch.

There is a subclass in CountDownLatch, named Sync, which inherits from AQS. The following is a snippet of the CountDownLatch code:

public class CountDownLatch {

    /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
        Sync(int count) {
            setState(count);
        }
    int getCount() {

        return getState();
    }

    protected int tryAcquireShared(int acquires) {

        return (getState() == 0) ? 1 : -1;

    }

    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;

        }

    }

}

private final Sync sync;

//省略其他代码…

}

It can be clearly seen that a Sync class is defined, which inherits AQS, which is exactly the “first step, create your own thread collaboration tool class, write a Sync class inside, and this Sync class inherits AbstractQueuedSynchronizer, which is AQS” mentioned in the previous section. In the CountDownLatch, there is also a variable named sync, which is an object of the Sync class.

At the same time, we can see that Sync not only inherits the AQS class, but also overrides the tryAcquireShared and tryReleaseShared methods, which corresponds to the second step mentioned, “design the collaboration logic of the thread collaboration tool class, in the Sync class, override the corresponding methods based on whether it is exclusive or not. If it is exclusive, override methods like tryAcquire or tryRelease; if it is non-exclusive, override methods like tryAcquireShared and tryReleaseShared.”

Here, CountDownLatch belongs to the non-exclusive type, so it overrides the tryAcquireShared and tryReleaseShared methods. But what do these two methods specifically mean? Don’t worry, next, let’s analyze the most important four methods in the CountDownLatch class, gradually unveiling its mystery.

Constructor #

First, let’s take a look at the constructor. CountDownLatch has only one constructor, which takes the number of “countdowns” as a parameter. Each time the countDown method is called, it will countdown by 1 until it reaches the initially set count. This is equivalent to “opening the latch”, so the threads waiting before can continue to work.

Let’s take a closer look at the constructor code:

public CountDownLatch(int count) {

    if (count < 0) throw new IllegalArgumentException("count < 0");

    this.sync = new Sync(count);

}

From the code, we can see that an exception is thrown when count < 0. When count >= 0, the code this.sync = new Sync(count) is executed. The count is passed into Sync class constructor, which is as follows:

Sync(int count) {

    setState(count);

}

This constructor calls the setState method of AQS and passes count into it. setState is the method that assigns a value to the state variable in AQS, as shown below:

protected final void setState(int newState) {
state = newState;
}

So we use the CountDownLatch constructor to pass the incoming count to the internal state variable in AQS, assign the value to state, and state represents the number of counts still needed.

getCount #

Next, let’s look at the getCount method, which is used to get the remaining count that still needs to be counted down. The source code of the getCount method is as follows:

public long getCount() {
    return sync.getCount();
}

This method returns the count of sync:

int getCount() {
    return getState();
}

By tracing the source code step by step, the getCount method calls getState of AQS:

protected final int getState() {
    return state;
}

As the code shows, the protected final int getState method directly returns the value of state, so in the end it gets the value of the state variable in AQS.

countDown #

Next, let’s take a look at the countDown method, which is actually the “release” method of CountDownLatch. Let’s see the source code:

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

In the countDown method, the releaseShared method of sync is called:

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

As we can see, releaseShared first checks with an if statement, checking the return result of the tryReleaseShared method. Therefore, let’s focus on the tryReleaseShared method, the source code is as follows:

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 method contains an infinite loop. In the loop body, the initial value of the variable `c` is obtained by using the `getState()` method. If `c` is equal to 0, it means that the countdown has reached zero, and the `return false` statement will be executed. If the `tryReleaseShared` method returns false, the `if (tryReleaseShared(arg))` block in the parent `releaseShared` method will be skipped, and false will be returned, indicating that `countDown` has no effect.

Moving on to the statements below `return false` in the `tryReleaseShared` method, if `c` is not equal to 0, `nextc` is assigned the value of `c-1`. Then, the `compareAndSetState` method is used to attempt to set `nextc` as the new value of `state`. If the assignment is successful, it means that the `countDown` operation in the `AQS` has succeeded, i.e., the value of the internal `state` variable has been decremented by 1. Finally, the statement `return nextc == 0` checks if `nextc` is 0. If so, it means that the countdown has reached the desired number, and the latch should be open. Therefore, the `tryReleaseShared` method returns true. In the parent `releaseShared` method, this will trigger the `doReleaseShared` method, which wakes up any blocked threads and allows them to continue execution.

Analyzing with specific numbers may provide a clearer understanding. Let's assume `c` is 2, which means that the countdown value is 2. In this case, `nextc` would be assigned the value 1, and then the `state` would be set to `nextc` using CAS. Assuming the assignment is successful, the statement `return nextc == 0` would return false because `nextc` is not equal to 0. This means that `countDown` has successfully modified the value of `state` by decrementing it by 1, but it has not awakened any threads.

The next time `countDown` is executed, the value of `c` would be 1, and `nextc` would be assigned the value 0. If the CAS operation is successful, the statement `return nextc == 0` would return true. This means that `state` has reached 0 after the countdown, causing the `tryReleaseShared` method to return true. In the `releaseShared` method, `doReleaseShared` is called to wake up all previously blocked threads.

#### await

Next, let's look at the `await` method, which is the "waiting" method of `CountDownLatch`. When the `await` method is called, the thread is blocked until the countdown reaches 0. The implementation of the `await` method can be seen by tracing the source code:

public void await() throws InterruptedException {

sync.acquireSharedInterruptibly(1);

}


It calls the `acquireSharedInterruptibly` method of `sync` and passes in 1. The source code of the `acquireSharedInterruptibly` method is as follows:
```java
public final void acquireSharedInterruptibly(int arg)

        throws InterruptedException {

if (Thread.interrupted())

    throw new InterruptedException();

if (tryAcquireShared(arg) < 0)

    doAcquireSharedInterruptibly(arg);

}

As we can see, besides handling interrupts, the important part is the tryAcquireShared method. This method simply checks if the value of getState is 0. If it is 0, it returns 1. Otherwise, it returns -1.

protected int tryAcquireShared(int acquires) {

    return (getState() == 0) ? 1 : -1;

}

The value returned by getState represents the remaining number of countdowns. If the remaining count is greater than 0, getState will not return 0. Therefore, tryAcquireShared will return -1. If tryAcquireShared returns -1, the condition if (tryAcquireShared(arg) < 0) will be true, and the doAcquireSharedInterruptibly method will be executed, causing the thread to enter a blocked state.

Let’s consider the other case. If state is already 0, it means that the countdown has already ended, and there is no need to wait anymore. In this case, getState will return 0, and tryAcquireShared will return 1. Once tryAcquireShared returns 1, acquireSharedInterruptibly will immediately return, which means that the await method will return immediately, and the thread will not enter a blocked state. It is as if the countdown has already ended and the thread can proceed.

The await and countDown methods correspond to the third step mentioned at the beginning of this lesson: “In your own thread coordination utility class, implement the relevant methods for acquiring/releasing and call the corresponding methods of AQS. If it is exclusive, call acquire or release methods; if it is non-exclusive, call acquireShared or releaseShared or acquireSharedInterruptibly methods.”

Summary of AQS Application in CountDownLatch #

Finally, let’s summarize the application of AQS in CountDownLatch. When a thread calls the await method of CountDownLatch, it attempts to acquire a “shared lock”. Normally, the lock cannot be acquired initially, so the thread is blocked. The “shared lock” can be acquired when the value of the “lock counter” is 0. The initial value of the “lock counter” is count. Each time the countDown method of the CountDownLatch object is called, the “lock counter” can be decremented by 1. By doing this, after calling the count times the countDown method, the “lock counter” becomes 0, and the previously waiting threads can continue running. If another thread wants to call the await method at this point, it will be immediately released and will not perform any blocking operations.

Summary #

In this lesson, we mainly discussed the usage of AQS, which typically involves three steps. Then, using CountDownLatch as an example, we demonstrated how to implement our own business logic using AQS.