75 What Is the Role and Importance of Aqsaqs

75 What Is the Role and Importance of AQSAQS #

In this lesson, we will mainly explain the importance of AQS, why we need AQS, and its role.

The importance of AQS #

Let’s first introduce the importance of AQS (AbstractQueuedSynchronizer) and see where AQS has been used in which classes.

并发1.png

As shown in the figure, AQS is used in ReentrantLock, ReentrantReadWriteLock, Semaphore, CountDownLatch, and ThreadPoolExecutor’s Worker (JDK 1.8). AQS is the underlying principle of these classes.

Many of these classes are commonly used, and most of them have been introduced in previous lessons. Therefore, it is self-evident that many important utility classes in the JUC package cannot do without the AQS framework. Hence, the importance of AQS is self-evident.

Learning approach for AQS #

Next, I would like to introduce my understanding of the learning approach for AQS. The internal structure of the AQS class is much more complex than that of ordinary classes. It has many details and is not easy to fully grasp. If we directly look at the source code, it is easy to get confused and get lost in the details, leading to frustration.

In fact, most of us programmers are business developers, not JDK developers, so we usually do not need to develop tool classes like ReentrantLock ourselves. Therefore, in general, we do not directly use AQS for development because the JDK has provided us with many well-encapsulated thread collaboration utility classes, such as ReentrantLock and Semaphore, which are provided by the JDK and internally use AQS. These utility classes already cover most business scenarios, so even if we do not understand AQS, we can still use these utility classes for development.

Since the purpose of learning AQS is not to develop code, why do we still need to learn AQS? I believe that the main purpose of learning AQS is to understand its principles and learn its design ideas in order to improve our skills and cope with interviews. Therefore, the main purpose of this lesson is to interpret AQS from a macro perspective. After understanding the macro ideas, we can analyze its internal structure more easily.

Locks and collaboration classes have something in common: Valve functionality #

Let’s start with familiar classes as the starting point for learning AQS. Please think about whether there are any commonalities between the ReentrantLock and Semaphore that we learned before.

In fact, both of them can be used as valves. For example, if we set the number of permits in Semaphore to 1, since it only has one permit, it can only allow one thread to pass. After the previous thread returns the permit, it will allow other threads to acquire the permit. In fact, this is similar to ReentrantLock, where only one thread can acquire the lock, and after this thread releases the lock, other threads will be allowed to acquire the lock. If a thread finds that there are no additional permits or cannot acquire the lock, it will be blocked and awakened when there are subsequent permits or when the lock is released. These processes are quite similar.

In addition to the aforementioned ReentrantLock and Semaphore, we will find that CountDownLatch, ReentrantReadWriteLock, and other utility classes have similar “collaboration” functionalities. In fact, they are all implemented using AQS.

Why do we need AQS #

With the above groundwork, let’s now think about why we need AQS.

The reason is that the aforementioned collaboration classes have many similar tasks. If the code that implements similar tasks can be extracted and turned into a new underlying utility class (or framework), then these utility classes can be directly used to build upper-level code, and this utility class is actually AQS.

With AQS, for thread collaboration utility classes such as ReentrantLock and Semaphore, they do not need to be concerned with so many thread scheduling details, they only need to implement their own design logic.

What if there is no AQS #

Let’s try to think in reverse. What if there is no AQS?

If there is no AQS, each thread collaboration utility class needs to implement at least the following content, including:

  • Atomic management of the state
  • Blocking and unblocking of threads
  • Management of queues

The meaning of the state here varies for different utility classes. For example, for ReentrantLock, it needs to maintain the number of times the lock has been reentered. However, the variable that holds the reentry count is accessed by multiple threads simultaneously, so it needs to be handled to ensure thread safety. Moreover, for those threads that fail to acquire the lock, they should be blocked and queued, and awakened at the appropriate time. So these contents are actually quite cumbersome and repetitive, and currently they are all handled by AQS.

If there is no AQS, classes like ReentrantLock would need to implement these relevant logic by themselves. However, it is quite difficult for each thread coordination utility class to correctly and efficiently implement these contents on their own. AQS can help us handle all the “dirty work”, so for classes like ReentrantLock and Semaphore, they only need to focus on their specific business logic. As the saying goes, “there is no such thing as a peaceful time, someone is carrying the weight for you”.

Analogy: HR and Interviewers #

If you don’t quite understand the role of AQS yet, please consider the following analogy. Let’s personify AQS and thread coordination utility classes as HR and interviewers.

Here, we simulate the scenario of candidates participating in campus recruitment interviews. For a company, an interview usually involves interviewers and HR. There are usually two types of interviews: group interviews and individual interviews. Group interviews refer to multiple students participating in the interview together. For example, if the rule is 10 people to be interviewed together, then the group interview rule is to gather 10 people first, and then conduct the interview together.

Concurrency2.png

Individual interviews, on the other hand, are usually conducted in a sequential, one-on-one manner. Let’s assume that there are 5 interviewers conducting individual interviews, i.e. these 5 interviewers are simultaneously interviewing one candidate each. During the interview process, the candidates will be queued up. When the front candidate finishes the interview, the next candidate will follow and start the interview with an available interviewer. This is the scenario of individual interviews.

Concurrency3.png

At first glance, the rules of group interviews and individual interviews are quite different: the former involves multiple people being interviewed together, while the latter is done one by one. However, there are actually many similarities (or processes or steps) between group interviews and individual interviews, and these similarities are often handled by HR. For example, when the interviewees arrive, HR needs to arrange them to sign in, sit and wait, and queue, and then HR needs to call them in order to avoid conflicts among multiple candidates and ensure that all waiting students will eventually be called. These series of tasks carried out by HR in the interview can be compared to what AQS does.

As for the specific interview rules, such as whether there are 5 or 10 people in a group interview, whether it is an individual or a group interview, these are determined by the interviewers. For interviewers, they won’t be concerned about whether there are conflicting numbers, how to wait, how to call, whether there are resting areas, etc., because these are within the scope of HR’s responsibilities.

The interviewers correspond to the custom implementation of the specific collaboration logic using AQS, while HR represents AQS. The mentioned rest for candidates refers to blocking threads, not continuously consuming CPU resources; and the subsequent call for candidates to attend interviews means waking up threads.

The process of group interviews is similar to CountDownLatch. CountDownLatch sets an initial value that needs to be counted down, for example, 10. Each time a candidate arrives, the count decreases by 1. If 10 people have arrived, the interviews will begin. Similarly, individual interviews can be understood as using Semaphore. Assuming there are 5 permits, each thread gets 1 permit at a time. This is similar to having 5 interviewers conducting interviews in parallel. Candidates need to acquire permits before the interview and return them after the interview.

For classes like CountDownLatch and Semaphore, they need to determine their own “recruiting” rules. Should they gather 10 candidates together for an interview, like a group interview? Or accept and interview one candidate at a time, like an individual interview? After determining the rules, the remaining tasks like greeting candidates (similar to scheduling threads) can be delegated to AQS. This way, the responsibilities are very independent and clear.

The Role of AQS #

After understanding the above, let’s summarize the role of AQS.

AQS is a framework for building locks, synchronizers, and other thread coordination utility classes. With AQS, many utility classes for thread coordination can be conveniently implemented. With AQS, higher-level development can greatly reduce workload, avoid reinventing the wheel, and also avoid thread safety issues caused by improper handling by higher-level developers, because AQS takes care of these things. In summary, with AQS, building thread coordination utility classes becomes much easier.

Conclusion #

In this lesson, we mainly introduced the approach to learning AQS, the reasons for needing AQS, and the role of AQS. With AQS, it becomes convenient to implement thread coordination utility classes, and AQS is widely used in the Java Concurrency Utilities package.