文章目录
前言
AQS(AbstractQueuedSynchronizer)
是java.util.concurrent(J.U.C)
包下的一个抽象类。concurrent
包下提供了一系列同步组件大大提高了并发的性能,包括ReentrantLock
、CountDownLatch
、semaphore
以及CyclicBarrier
等,这些同步组件都直接或间接基于 AQS 提供的独占锁、共享锁和等待队列实现了各自的同步需求,所以 AQS 被认为是 J.U.C
的核心。
一、AQS 简介
AQS 全名:AbstractQueuedSynchronizer,是java.util.concurrent.locks
包下的一个类。
- 在同步组件的实现中,AQS 是核心部分,同步组件通过使用 AQS 提供的模板方法实现同步组件的语义;
- AQS 则实现了对同步状态的管理、对阻塞线程进行排队以及等待和唤醒等一些底层的实现处理;
- AQS 的核心主要包括:同步队列、独占锁的获取和释放、共享锁的获取和释放、可中断锁,以及超时等待获取锁这些特性的实现,而这些特性实际上是 AQS 提供的模板方法。
二、AQS 源码分析
1. AQS 的核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态;如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,AQS 就是通过 CLH 队列来实现这个机制的,也就是将暂时获取不到锁的线程加入到等待队列中。
2. 资源 state
AQS 使用一个 int
型的成员变量来表示同步状态,即 state
(上图中的资源 state),内部声明如下:
/**
* The synchronization state.
*/
private volatile int state;
不同的 state
值表示不同的状态,对 state
变量值的修改由其子类实现。比如,在 ReentrantLock
中,state == 0
表示还没有线程获取锁,state == 1
表示有线程获取了锁,state > 1
表示重入锁的数量。
AQS 的两个方法 acquire(int arg)
和 release(int arg)
分别通过增大和减小 state
的值来获取和释放一定量的资源。子类通过继承 AQS 并实现这两个方法管理状态:
- 在
acquire()
方法中,若state
的大小符合特定需求(具体逻辑由子类实现),则线程会锁定同步器;否则,将当前线程加入到同步队列中。 - 在
release()
方法中,若state
的大小符合特定需求,则释放掉当前线程占有的资源,唤醒同步队列中的线程。
状态信息通过 AQS 的 getState()
、setState()
、compareAndSetState()
进行操作,方法的源码如下:
/**
* Returns the current value of synchronization state.
*
* 返回同步状态的当前值
*/
protected final int getState() {
return state;
}
/**
* Sets the value of synchronization state.
*
* 设置同步状态的值
*/
protected final void setState(int newState) {
state = newState;
}
/**
* Atomically sets synchronization state to the given updated
* value if the current state value equals the expected value.
*
* 如果当前同步状态的值等于期望值(expect),通过CAS操作,将同步状态值设定为给定值update
*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
3. 独占锁与共享锁
concurrent
包下提供的同步组件的加锁模式分为独占锁和共享锁。独占锁模式下,每次只能有一个线程持有锁,例如ReentrantLock
实现的就是独占锁;共享锁模式下,则允许多个线程同时获取锁,并发地访问共享资源,例如ReadWriteLock
、countdownlatch
、semaphore
等组件实现的是共享锁,锁的数量由用户指定。
显然,独占锁是一种悲观保守的加锁策略。如果某个只读线程获取锁,则其它读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。而共享锁则是一种乐观锁,它放宽了加锁策略,允许多个线程同时访问资源。比如并发包下的
ReadWriteLock
(读写锁),它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时执行。
独占锁:
void acquire(int arg)
:独占式获取同步状态,如果获取失败则插入同步队列等待;void acquireInterruptibly(int arg)
:与acquire()
方法相同,但在同步队列中等待时可以响应中断;boolean tryAcquireNanos(int arg, long nanosTimeout)
:在可响应中断的acquire()
方法基础上增加了超时功能,在超时时间内成功获取锁则返回 true,否则返回 false;boolean tryAcquire(int arg)
:尝试获取锁,获取锁成功返回 true,否则返回 false;boolean release(int arg)
:释放同步状态,该方法会唤醒在同步队列中的下一个节点(线程)。
共享锁:
void acquireShared(int arg)
:共享式获取同步状态,与独占锁的区别在于同一时刻有多个线程获取同步状态;void acquireSharedInterruptibly(int arg)
:增加了响应中断的功能;boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
:增加了超时等待功能;boolean releaseShared(int arg)
:共享锁释放同步状态。
4. 同步队列
当共享资源被某个线程占有,其它请求该资源的线程将会阻塞,从而进入同步队列。就数据结构而言,AQS 实现的是一个 FIFO 的队列,底层实现是一个带头节点的双向链表。如下图所示:
AQS 有一个静态内部类 Node
,定义了同步队列中每个具体的节点。部分源码如下:
static final class Node {
// 标识共享模式的节点,共享模式下Node节点的nextWaiter变量设置为这个值
static final Node SHARED = new Node();
// 标识独占模式的节点,独占模式nextWaiter变量是null
static final Node EXCLUSIVE = null;
// 当前节点从同步队列中取消
static final int CANCELLED = 1;
// 后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继 节点的线程继续运行
static final int SIGNAL = -1;
// 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了 signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。
static final int CONDITION = -2;
// 表示下一次共享式同步状态将会无条件地被传播下去
static final int PROPAGATE = -3;
/**
* 节点状态
*/
volatile int waitStatus;
/**
* Link to predecessor node that current node/thread relies on
* for checking waitStatus.
*
* 当前节点的前驱节点
*/
volatile Node prev;
/**
* Link to the successor node that the current node/thread
* unparks upon release.
*
* 当前节点的后继节点
*/
volatile Node next;
/**
* The thread that enqueued this node.
*
* 当前节点锁包装的线程对象
*/
volatile Thread thread;
/**
* Link to next node waiting on condition, or the special
* value SHARED.
*
* 等待队列中的下一个节点
*/
Node nextWaiter;
}
类中的几个 static final
变量表示节点的状态,几个由 volatile
修饰的属性表示每个节点的属性。可以看到每个节点都有前驱节点和后继节点,所以同步队列是一个双向链表。AQS 实际上通过头尾指针来管理同步队列,同时实现包括将获取锁失败的线程入队、释放锁时唤醒同步队列中的等待线程等和新方法。
5. 独占锁的获取
下面我们通过
ReentrantLock
的源码分析独占锁的获取和释放。
调用 lock()
方法获取的是独占锁,获取成功则线程执行,获取失败则调用 AQS 提供的 acquire(int arg)
模板方法将当前线程加入同步队列。 lock()
方法源码:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
lock()
方法使用 CAS 操作尝试将同步状态改为 1,如果成功则将同步状态持有线程置为当前线程,否则就调用 AQS 提供的 acquire(int arg)
方法。
public final void acquire(int arg) {
// 再次尝试获取同步状态,如果成功则方法直接返回
// 如果失败则先调用addWaiter()方法再调用acquireQueued()方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(int arg)
:再次尝试获取同步状态,获取成功方法直接返回;获取失败则调用 addWaiter()
方法;
addWaiter(Node.EXCLUSIVE, arg)
:将当前线程以指定模式(独占式、共享式)封装为 Node
节点并将其入队。
private Node addWaiter(Node mode) {
// 将线程以指定模式封装为Node节点
Node node = new Node(Thread.currentThread(), mode);
// 获取当前队列的尾节点
Node pred = tail;
// 若尾节点不为空
if (pred != null) {
node.prev = pred;
// 使用CAS将当前节点尾插到同步队列中
if (compareAndSetTail(pred, node)) {
pred.next = node;
// CAS尾插成功,返回当前Node节点
return node;
}
}
// 尾节点为空 || CAS尾插失败
enq(node);
return node;
}
分析 addWaiter()
方法,程序的逻辑主要分为两部分:
- 当前同步队列的尾节点为
null
,则调用enq()
方法插入节点; - 当前同步队列的尾节点不为
null
,则采用尾插法将当前封装的节点插入到同步队列中。当然,如果if(compareAndSetTail(pred, node))
为 false 的话,则说明 CAS 操作失败,就会继续执行到enq()
方法。
下面我们再看 enq()
方法:
private Node enq(final Node node) {
// 直到将当前节点插入同步队列成功为止
for (;;) {
Node t = tail;
// 初始化同步队列
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 不断CAS将当前节点尾插入同步队列中
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
我们知道同步队列是带头节点的链式存储结构,在创建 Node
类时就创建了头节点。带头节点的队列的初始化时机是:当 tail == null
时,即第一次往同步队列插入线程的时候。并且 enq()
方法中是一个 for(;;)
死循环,compareAndSetTail(t, node)
方法会利用 CAS 操作操作设置尾节点,如果失败则在循环中不断重新尝试,直至成功返回。
因此, enq()
方法的两个功能:
- 在当前线程是第一个加入同步队列时,调用
compareAndSetHead(new Node())
方法,完成链式队列头节点的初始化; - 自旋不断尝试 CAS 尾插入节点直至成功。
通过上面的分析我们已经知道了将获取独占锁失败的线程包装成 Node 节点然后插入同步队列的过程。下面我们来看看:在同步队列中的节点(线程)是怎样保证自己有机会获得独占锁的。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 获取同步状态成功条件
// 前驱节点为头结点并且获取同步状态成功
if (p == head && tryAcquire(arg)) {
// 将当前节点设置为头结点
setHead(node);
// 删除原来的头结点
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 获取失败将当前节点取消
if (failed)
cancelAcquire(node);
}
}
acquireQueued()
方法内部依然是是一个通过 for(;;)
自旋的过程:首先获取当前节点的前驱节点,如果前驱节点是头节点并且成功获得同步状态时(if(p == head && tryAcquire(arg))
),表示当前节点指向的线程能够获取锁;反之,获取锁失败则进入等待状态,先不断自旋将前驱节点状态置为 SIGINAL
,然后调用 LockSupport.park()
方法将当前线程阻塞。 下面我们详细分析这两个处理过程:
获取锁成功并将头节点出队的过程:
AQS 实际是通过头尾指针来管理同步队列的,示意图如下:
我们看 acquireQueued()
方法内获取锁成功并将头节点出队的逻辑:
// 当前节点前驱为头结点并且再次获取同步状态成功
if (p == head && tryAcquire(arg)) {
//队列头结点引用指向当前节点
setHead(node);
//释放前驱节点
p.next = null; // help GC
failed = false;
return interrupted;
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
将当前节点通过 setHead()
方法设置为队列的头节点,然后将之前的头节点的 pre
和 next
均指向 null
,即将其与队列断开,垃圾回收器会将其回收。如下图:
获取锁失败后自旋处理的过程:
当节点在同步队列中获取锁失败的时候会调用 shouldParkAfterFailedAcquire()
方法。该方法的主要逻辑是:使用 CAS 操作将前驱节点的状态由 INITIAL
置为 SIGNAL
,表示需要将当前节点阻塞。如果失败,则会在 acquireQueued()
方法中自旋直到将前驱节点状态置为 SIGNAL
为止。只有当 shouldParkAfterFailedAcquire()
方法返回 true 时才会执行 parkAndCheckInterrupt()
方法。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
该方法会调用 LookSupport.park()
方法,用来阻塞当前线程。
总结下来,acquireQueued()
在自旋过程中主要完成了两件事:
- 如果当前节点的前驱节点是头节点,并且能够获得同步状态的话,当前线程能够获得锁并在该方法执行结束后退出;
- 获取锁失败的话,先将节点的状态置为
SIGNAL
,然后调用LookSupport.park()
方法将当前线程阻塞。
6. 独占锁的释放(release() 方法)
独占锁释放锁时调用了 unlock()
方法,unlock()
方法实际上调用了 AQS 的 release()
方法:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
}
代码逻辑为:如果同步状态释放成功(tryRelease()
返回 true),则会执行 if
块中的代码。当 head 指向的头节点不为 null,且该节点的状态值不为 0 时才执行 unparkSuccessor()
方法。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 头结点的后继节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 后继节点不为null时唤醒
LockSupport.unpark(s.thread);
}
首先获取头节点的后继节点,当后继节点不为空时会调用 LookSupport.unpark()
方法,该方法会唤醒该节点的后继节点所包装的线程。因此,每一次释放锁之后,都会唤醒队列中该节点的后继节点所引用的线程。
7. 独占锁获取与释放总结
- 线程获取锁失败,则调用
addWaiter()
方法将线程封装成 Node 进行入队操作。addWaiter()
方法内部会调用enq()
方法完成对同步队列头节点的初始化以及 CAS 尾插失败的自旋处理。 - 入队之后排队获取锁的核心方法是
acquireQueued()
方法,节点排队获取锁是一个自旋过程。当且仅当当前节点的前驱节点为头节点并且成功获取同步状态时,头节点出队,当前节点引用的线程获取到锁。否则,不满足条件时就不断自旋将前驱节点的状态置为SIGNAL
后调用LockSupport.part()
方法将线程阻塞。 - 释放锁时会唤醒后继节点。