1. Introduction to AQS
The AbstractQueuedSynchronizer abstract class (hereinafter referred to as AQS) is java.util.concurrent
the core of the entire package. In JDK1.5, Doug Lea introduced the JUC package, and most of the synchronizers in the package were built based on AQS. The AQS framework provides a set of general mechanisms to manage synchronization state, block/wake up threads, and manage waiting queues.
The well-known synchronizers such as ReentrantLock, CountDownLatch, CyclicBarrier, etc. actually implement the APIs exposed by the AQS framework through internal classes to achieve various synchronizer functions. The main difference between these synchronizers is actually the definition of the synchronization state.
The AQS framework separates a series of concerns when constructing a synchronizer. All its operations revolve around the resource-synchronization state (synchronization state), and solve the following problems for users:
- Can resources be accessed at the same time? Or can only be accessed by one thread at the same time? (Shared/Exclusive Function)
- How to concurrently manage threads that access resources? (Waiting queue)
- If the thread can't wait for resources, how to exit from the waiting queue? (Timeout/interrupt)
This is actually a typical template method design pattern: the parent class (AQS framework) defines the skeleton and internal operation details, and the specific rules are implemented by the subclass.
The AQS framework leaves one remaining question to the user:
what is a resource? How to define whether the resource can be accessed?
Let's take a look at the definition of this problem for several common synchronizers:
Synchronizer | Definition of resources |
---|---|
ReentrantLock | The resource represents an exclusive lock. State 0 means the lock is available; 1 means occupied; N means the number of reentrants |
CountDownLatch | The resource represents a countdown counter. State of 0 means that the counter is reset to zero, and all threads can access resources; N means that the counter is not reset to zero, and all threads need to be blocked. |
Semaphore | Resources represent semaphores or tokens. State ≤ 0 means that no token is available and all threads need to be blocked; greater than 0 means that the token is available. Each time the thread acquires a token, the State is reduced by 1, and the thread does not release a token, and the State is increased by 1. |
ReentrantReadWriteLock | Resources represent shared read locks and exclusive write locks. The state is logically divided into two 16-bit unsigned shorts, which record the number of threads used by the read lock and the number of times the write lock is reentered. |
In summary, the AQS framework provides the following functions:
1.1 Provide a set of template framework
Due to the existence of concurrency, there are many situations that need to be considered. Therefore, whether these two goals can be achieved in a relatively simple way is very important, because for users (users of the AQS framework), many times do not care Intricate details inside. AQS actually uses the template method pattern to achieve this. Most of the methods in AQS are final or private, which means that Doug Lea does not want users to use these methods directly, but only overwrites some of the methods specified by the template. .
AQS allows users to solve the above-mentioned " how to define whether the resource can be accessed " problem by exposing the following APIs :
Hook method | description |
---|---|
tryAcquire | Exclusive access (number of resources) |
tryRelease | Exclusive release (number of resources) |
tryAcquireShared | Shared acquisition (number of resources) |
tryReleaseShared | Shared acquisition (number of resources) |
isHeldExclusively | Exclusive status |
1.2 Support interruption, timeout
Remember those methods of lock interruption, time-limited waiting, and lock attempt in the Lock interface? The realization of these methods is actually provided by AQS built-in.
All synchronizers using the AQS framework support the following operations:
- Blocking and non-blocking (such as tryLock) synchronization;
- Optional timeout setting, so that the caller can give up waiting;
- Interruptible blocking operation.
1.3 Support exclusive mode and shared mode
1.4 Support Condition wait
The Condition interface can be seen as a substitute for the wait(), notify(), and notifyAll() methods of the Obechct class, and is used in conjunction with Lock.
The AQS framework ConditionObject
implements the Condition interface through an internal class to provide the function of conditional waiting for subclasses.
Two, AQS method description
As mentioned in the first part of this chapter, AQS uses the template method pattern, most of which are final or private. We call this type of method * Skeleton Method* , which means that these methods are defined by the AQS framework itself. Skeleton, sub-categories cannot be overwritten.
The following will briefly describe some of the more important methods by category, and the specific implementation details and principles will be explained in detail in the subsequent parts of this series.
2.1 CAS operation
CAS, that is, CompareAndSet, the implementation of CAS operations in Java is entrusted to a class named UnSafe. Regarding the Unsafe
class, this class will be introduced in detail in the future. At present, as long as you know, atomic operations on fields can be achieved through this class.
Method name | Modifier | description |
---|---|---|
compareAndSetState | protected final | CAS modify synchronization state value |
compareAndSetHead | private final | CAS modifies the head pointer of the waiting queue |
compareAndSetTail | private final | CAS modifies the tail pointer of the waiting queue |
compareAndSetWaitStatus | private static final | CAS modifies the waiting state of the node |
compareAndSetNext | private static final | CAS modifies the next pointer of the node |
2.2 The core operation of the waiting queue
Method name | Modifier | description |
---|---|---|
enq | private | Enqueue operation |
addWaiter | private | Enqueue operation |
setHead | private | Set head node |
unparkSuccessor | private | Wake up successor node |
doReleaseShared | private | Release shared node |
setHeadAndPropagate | private | Set the head node and propagate the wake-up |
2.3 Obtaining resources
Method name | Modifier | description |
---|---|---|
cancelAcquire | private | Cancel access to resources |
shouldParkAfterFailedAcquire | private static | Determine whether to block the current calling thread |
acquireQueued | final | Try to get resources, get failed, try to block the thread |
doAcquireInterruptibly | private | Exclusive access to resources (response to interruption) |
doAcquireNanos | private | Obtain resources exclusively (wait for a limited time) |
doAcquireShared | private | Shared access to resources |
doAcquireSharedInterruptibly | private | Shared access to resources (response to interruption) |
doAcquireSharedNanos | private | Shared resources (waiting for a limited time) |
Method name | Modifier | description |
---|---|---|
acquire | public final | Exclusive access to resources |
acquireInterruptibly | public final | Exclusive access to resources (response to interruption) |
acquireInterruptibly | public final | Obtain resources exclusively (wait for a limited time) |
acquireShared | public final | Shared access to resources |
acquireSharedInterruptibly | public final | Shared access to resources (response to interruption) |
tryAcquireSharedNanos | public final | Shared resources (waiting for a limited time) |
2.4 Release operations of resources
Method name | Modifier | description |
---|---|---|
release | public final | Release exclusive resources |
releaseShared | public final | Release shared resources |
3. Brief description of AQS principle
As we mentioned in the first section, the AQS framework separates a series of concerns when constructing a synchronizer. All its operations revolve around the resource-the synchronization state. Therefore, around the resource, there are three derived Basic question:
- Management of synchronization state
- Blocking/waking up threads
- Thread waiting queue management
3.1 Synchronization status
Definition of
synchronization state The synchronization state is actually a resource. AQS uses a single int (32 bits) to save the synchronization state, and exposes getState, setState, and compareAndSetState operations to read and update this state.
/**
* 同步状态.
*/
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
/**
* 以原子的方式更新同步状态.
* 利用Unsafe类实现
*/
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
3.2 Thread blocking/waking up
Before JDK 1.5, apart from the built-in monitor mechanism, there was no other way to safely and conveniently block and wake up the current thread.
After JDK1.5, the java.util.concurrent.locks package provides the LockSupport class as a tool for thread blocking and wake-up.
3.3 等待队列
等待队列,是AQS框架的核心,整个框架的关键其实就是如何在并发状态下管理被阻塞的线程。
等待队列是严格的FIFO队列,是Craig,Landin和Hagersten锁(CLH锁)的一种变种,采用双向链表实现,因此也叫CLH队列。
1. 结点定义
CLH队列中的结点是对线程的包装,结点一共有两种类型:独占(EXCLUSIVE)和共享(SHARED)。
每种类型的结点都有一些状态,其中独占结点使用其中的CANCELLED(1)、SIGNAL(-1)、CONDITION(-2),共享结点使用其中的CANCELLED(1)、SIGNAL(-1)、PROPAGATE(-3)。
结点状态 | 值 | 描述 |
---|---|---|
CANCELLED | 1 | 取消。表示后驱结点被中断或超时,需要移出队列 |
SIGNAL | -1 | 发信号。表示后驱结点被阻塞了(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。) |
CONDITION | -2 | Condition专用。表示当前结点在Condition队列中,因为等待某个条件而被阻塞了 |
PROPAGATE | -3 | 传播。适用于共享模式(比如连续的读操作结点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。) |
INITIAL | 0 | 默认。新结点会处于这种状态 |
AQS使用CLH队列实现线程的结构管理,而CLH结构正是用前一结点某一属性表示当前结点的状态,之所以这种做是因为在双向链表的结构下,这样更容易实现取消和超时功能。
next指针:用于维护队列顺序,当临界区的资源被释放时,头结点通过next指针找到队首结点。
prev指针:用于在结点(线程)被取消时,让当前结点的前驱直接指向当前结点的后驱完成出队动作。
static final class Node {
// 共享模式结点
static final Node SHARED = new Node();
// 独占模式结点
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
/**
* INITAL: 0 - 默认,新结点会处于这种状态。
* CANCELLED: 1 - 取消,表示后续结点被中断或超时,需要移出队列;
* SIGNAL: -1- 发信号,表示后续结点被阻塞了;(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。)
* CONDITION: -2- Condition专用,表示当前结点在Condition队列中,因为等待某个条件而被阻塞了;
* PROPAGATE: -3- 传播,适用于共享模式。(比如连续的读操作结点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。)
*
* waitStatus表示的是后续结点状态,这是因为AQS中使用CLH队列实现线程的结构管理,而CLH结构正是用前一结点某一属性表示当前结点的状态,这样更容易实现取消和超时功能。
*/
volatile int waitStatus;
// 前驱指针
volatile Node prev;
// 后驱指针
volatile Node next;
// 结点所包装的线程
volatile Thread thread;
// Condition队列使用,存储condition队列中的后继节点
Node nextWaiter;
Node() {
}
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
}
2. 队列定义
对于CLH队列,当线程请求资源时,如果请求不到,会将线程包装成结点,将其挂载在队列尾部。
CLH队列的示意图如下:
①初始状态,队列head和tail都指向空
②首个线程入队,先创建一个空的头结点,然后以自旋的方式不断尝试插入一个包含当前线程的新结点
/**
* 以自旋的方式不断尝试插入结点至队列尾部
*
* @return 当前结点的前驱结点
*/
private Node enq(final Node node) {
for (; ; ) {
Node t = tail;
if (t == null) { // 如果队列为空,则创建一个空的head结点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
四、总结
本章简要介绍了AQS的思想和原理,读者可以参考Doug Lea的论文,进一步了解AQS。
本文参考:https://segmentfault.com/a/1190000015562787
关注公众号,输入“java-summary”即可获得源码。
完成,收工!
【传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。