一. AQS概述
在Java的concurrent包中,基本上并发工具都是使用了AbstractQueuedSynchronizer类(简称AQS)作为核心,因此AQS也是并发编程中最重要的地方。同步器AQS是实现锁的关键,锁和AQS很好隔离了二者所需关注的领域:
- 锁的API面向使用者,定义了与锁交互的公共行为
- 锁的实现是依托给AQS实现的,AQS面向的是线程访问和资源控制
AQS中采用了一个状态位state + 一个FIFO的队列的方式,记录了锁的获取,释放等。
AQS支持共享模式和独占模式,在独占模式下,其他线程试图获取该锁将无法获取成功;在共享模式下,多个线程获取某个锁可能(但不是一定)会获取成功
Java大神Doug Lea对AQS的解析:The java.util.concurrent Synchronizer Framework
二. AQS结构
AQS的实现依赖内部的同步队列(FIFO双向队列)来完成同步状态的管理,假如当前线程获取同步状态失败,AQS会将该线程以及等待状态等信息构造成一个Node,并将其加入同步队列,同时阻塞当前线程。当同步状态释放时,唤醒队列的首节点。队列初始化时,头节点head是创建的空Node:new Node()
2.1 状态state
AQS使用一个简单的原子int值为大多数同步器表示状态,子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。在互斥锁中它表示着线程是否已经获取了锁,0未获取,1已经获取了,大于1表示重入数。
- AQS提供了getState()和setState()方法获取/设置状态,是线程可见性的
- AQS提供了compareAndSetState()方法以原子方式更新state值
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2.2 队列元素Node
AQS实现依赖一个FIFO队列,队列元素为Node,Node保存着线程引用和线程状态
2.2.1 元素Node
static final class Node {
// 当前Node的状态
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
volatile Thread thread;
}
2.2.2 元素Node状态waitStatus
waitStatus表示队列元素Node的状态
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
- waitStatus = 1 : 在同步队列中等待的线程等待超时或者被中断时需要被取消,此状态Node会保持取消状态不变,会被踢出队列,等待GC
- waitStatus = -1:当前节点Node的后继节点由于被阻塞,需要一个信号通知它解除阻塞
- waitStatus = -2:当前节点Node在等待队列中,在等待某个条件而被阻塞
- waitStatus = -3:只用在共享模式下,表示当前场景下后续的acquireShared能够得以执行
- waitStatus = 0:默认值,表示当前节点在队列中,等待获取锁
三. 获取独占锁
独占锁,处于独占模式下,其他线程试图获取该锁将无法取得成功。
首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现
3.1 获取独占锁acquire
- tryAcquire首先尝试获取独占锁,如果获取成功就返回
- 尝试获取失败就将当前线程以独占模式加入队列addWaiter
- acquireQueued使线程在等待队列中获取资源,获取失败就阻塞当前线程,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
3.2 加入队列addWaiter
- 当尝试获取锁失败,就根据当前线程以独占模式创建Node加入队列
- 如果队列尾节点存在,就使用CAS(compareAndSetTail)将node直接放入队列
- 如果2执行失败或tail为null,执行enq,使用CAS自旋方式将node加入队列,死循环直到成功
3.1 如果尾节点为null,说明队列为空,head也为null,node先CAS加入队列,head指向node
3.2 如果队列尾节点存在,就使用CAS(compareAndSetTail)将node直接放入队列
3.3 死循环执行,直到执行成功
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {// 入队列
pred.next = node;
return node;
}
}
enq(node);
return node;
}
3.3 初始化队列
最开始时AQS队列还是空的,即head和tail都为null,死循环处理:
- 首先初始化队列,初始head和tail
- 队列初始化完成,继续循环,node节点添加进队列
private Node enq(final Node node) {
for (;;) {// 自旋
Node t = tail;
if (t == null) { // 队列为空,必须先初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {// 加入队列
t.next = node;
return t;
}
}
}
}
3.3.1 队列初始化
当队列第一次有节点添加,此时tail为null,需要先初始化head
- 先创建一个空节点:new Node()
- 使用CAS将刚创建的空节点设置为head,作为头节点
- 初始尾节点tail和头节点head相同
3.3.2 节点加入队列
队列初始化后,会继续循环,将node节点添加进队列
- 首先node前驱引用prev指向tail:node.prev = t
- 使用CAS将设置node为尾节点
- 将头节点head的next引用指向node节点
3.3 acquireQueued
addWaiter(node)方法将当前线程加入队列尾部了,此时node已经在队列中了,接下来就出阻塞当前线程,等待锁的获取
当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,原因有二:
- 头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点
- 维护同步队列的FIFO原则
如果线程A从同步队列获取到锁,则此时线程A对应的节点Node是头节点,当线程A执行完成释放锁,会唤醒线程A对应节点的后继节点 - 线程B对应节点,线程B节点在自旋时获取到锁(acquireQueued),此时头结点head会指向线程B节点,切断线程A节点的next引用,则线程A对应节点就从同步队列移除了
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);
}
}
节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程)
setHead() 设置头结点(首节点)
3.4 是否挂起线程shouldParkAfterFailedAcquire
在线程尝试获取锁失败后,需要判断当前节点是否应该被挂起:
1. 上面介绍过(2.2.2),节点有5种状态,只有当前节点node的前驱节点pred的状态waitStatus为-1,即pred的后继节点需要信号才能被唤醒,表示pred的后继节点node需要pred的唤醒,所以node需要被挂起park,返回true
2. 当节点pred状态大于0(如-1表示节点状态为取消状态),说明节点pred是无效节点。一直根据pred上驱节点遍历,将无效节点从队列中清除,直到某个上驱节点状态不大于0,由于node的前驱节点发生了改变,需要继续3.3中的循环,继续判断,所以返回false
3. 当前驱节点pred状态不大于0且不等于-1(节点状态为0、-2、-3),则使用CAS将pred的状态修改为-1,需要继续3.3中的循环,继续判断,所以返回false
所以节点node的前驱节点状态是SIGNAL(-1),那node就可以安心的休息了(被挂起),否则,就需要先处理node的前驱节点,返回acquireQueued的死循环中继续判断
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {// ws>0表示节点pred被取消了,需要清除取消状态的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
3.5 线程挂起并检查中断状态parkAndCheckInterrupt
如果要取消一个线程的排队,我们需要在另外一个线程中对其进行中断
当判断节点线程需要被挂起,执行parkAndCheckInterrupt挂起当前线程,并检查线程中断状态
- 线程的挂起是通过基本线程阻塞原语LockSupport.park来实现的
- 执行Thread.interrupted()并返回
- 注意:Thread.interrupted()测试当前线程是否处于中断状态,返回线程中断状态,并将线程中断标志位设置为false
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();// 设置中断标志位为false
}
即使前驱节点是头结点,但是tryAcquire方法也未必会成功,对于非公平锁来说新来一个节点会优先去tryAcquire竞争锁资源,如果成功了,那么这个前驱为头结点的节点进行tryAcquire就会失败。
3.5.2 挂起线程被唤醒
线程调用LockSupport.park(this)被挂起,下面三种情况会唤醒线程:
- 其他线程中以被挂起线程为目标调用unpark
- 其他线程中中断被挂起线程
- 虚假呼叫,即无理由返回
3.5.2 a.interrupt()和Thread.interrupted()
不是说中断某个线程,这个线程就停止运行了。中断代表线程状态,每个线程都关联了一个中断状态,是一个 true 或 false 的 boolean 值,初始值为 false
1. A线程第一次调用a.interrupt(),则A线程中断标志位设置为true
2. 接着调用Thread.interrupted(),测试当前线程是否处于中断状态。由于当前线程A的中断标志位true,会返回true。执行后会将线程A中断状态标志位设置为false
3. 再次接着调用Thread.interrupted(),由于当前线程A的中断标志位false,返回false,执行后会将线程A中断状态标志位设置为false
public class Test {
public static void main(String[] args) {
Thread.currentThread().interrupt();
System.out.println("第一次调用Thread.interrupted(),返回值:"+Thread.interrupted());
System.out.println("第二次调用Thread.interrupted(),返回值:"+Thread.interrupted());
}
}
输出:
第一次调用Thread.interrupted(),返回值:true
第二次调用Thread.interrupted(),返回值:false
3.6 取消请求cancelAcquire
当node节点在acquireQueued死循环过程中发生异常(如线程被中断),就需要取消获取,对应到队列操作,就需要将node节点从队列中移除
- 如果node为null,不需要处理,直接返回
- 将node中线程的引用置为null,切断引用
- 处理node的前驱节点,将前驱节点状态为1的节点从队列移除,直到找到状态不为取消的节点pre,node节点的前驱引用prev指向pre
- 将node节点状态置为1,即取消状态
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
Node pred = node.prev;
while (pred.waitStatus > 0) // 剔除取消状态的节点
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED;
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
3.6.1 node为尾节点
当node节点是队列的尾节点,要将node节点从队列移除:
- node是尾节点,首先使用CAS尝试将node前驱节点pred设置为尾节点,即tail引用指向pred
- compareAndSetTail(node, pred)成功则尾节点更新了,tail指向pred,
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
}
3.6.2 node不为尾节点
当node不为队列的尾节点, 需要将node从队列中间移除
- 前驱节点pred不是头节点,前驱节点状态为signal或前驱节点状态小于0且成功将前驱节点状态设置为signal,前驱节点包含一个线程,使用CAS修改前驱节点的next引用
- 否则执行唤醒node节点的后继节点
- node节点next引用指向自己,帮助GC节点node
3.6.3 唤醒node后继节点unparkSuccessor
- 如果node存在后继节点s,唤醒s
- 否则从尾节点向前遍历找到未取消的节点,唤醒
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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)
LockSupport.unpark(s.thread);
}
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问。
共享锁原理:
共享锁的基本流程与独占锁相同,主要区别在于判断锁获取的条件上,由于是共享锁,也就允许多个线程同时获取,所以同步状态的数量同时的大于1的,如果同步状态为非0,则线程就可以获取锁,只有当同步状态为0时,才说明共享数量的锁已经被全部获取,其余线程只能等待。
共享锁的释放过程正好与之相反,释放锁对应的AQS操作时增加同步状态的值。
四.释放独占锁
当前线程获取同步状态并执行了相应的逻辑之后,就需要释放同步状态,让后续节点可以获取到同步状态,调用方法release(int arg)方法可以释放同步状态
- 尝试释放状态,tryRelease保证将状态重置回去,同样采用CAS来保证操作的原子性
- 释放成功,调用unparkSuccessor(见#3.6.3)唤醒头结点head的后续节点,返回true表示释放成功
- 否则返回释放失败
如果线程A从同步队列获取到锁,则此时线程A对应的节点Node是首节点,当线程A执行完成释放锁,会唤醒线程A对应节点的后继节点 - 线程B对应节点,线程B节点在自旋时获取到锁(acquireQueued),此时头结点head会指向线程B节点,切断线程A节点的next引用,则线程A对应节点就从同步队列移除了
六. 等待队列Condition
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、 wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的
6.1 Condition使用实例
- Condition定义了等待/通知两种类型的方法:await/signal
- Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的
- 当前线程调用这些方法时,需要提前获取到Condition对象关联的锁
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
6.2 ConditionObject
ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要
获取相关联的锁,所以作为同步器的内部类也较为合理
- 每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键
- 等待队列是一个FIFO的队列,等待队列中的每个节点都包含了一个线程引用,该线程就是
在Condition对象上等待的线程 - Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
private transient Node firstWaiter;
private transient Node lastWaiter;
public ConditionObject() { }
}
6.3 条件队列等待 - await
如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态 (因为调用了LockSupport.park(this)方法,所以当前线程状态是WAITING)。
事实上,Node节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node 。
当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的头节点(获取了锁的节点)移动到Condition的等待队列中。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); // 当前线程加入等待队列
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// 当前线程加入等待队列
private Node addConditionWaiter() {
Node t = lastWaiter;
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null) // 第一次调用await
firstWaiter = node;
else
t.nextWaiter = node; // 单链表
lastWaiter = node;
return node;
}
// 移除等待队列中状态不是Node.CONDITION的节点
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
6.3.1 第一次await
Condition拥有头尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的
6.3.2 多次await
在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列
AQS实质上拥有一个同步队列和多个等待队列,具体对应关系如下图所示:
6.3.3 移除非等待状态的节点
将当前线程加入等待线程时,会先判断尾节点状态是否为-2(即Node.CONDITION 等待状态),如果不是,会从头结点firstWaiter开始遍历,移除等待队列状态不是-2的节点
6.3.4 释放锁
await方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态 。
当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException
// 释放同步状态,也就是释放锁
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {// 释放锁,唤醒后继节点
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
6.4 条件队列唤醒 - signal
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(头节点),在唤醒节点之前,会将节点移到同步队列中的尾部位置。
- 当前线程必须是获取了锁的线程
- 获取等待队列的头节点
- 通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程
- 被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法 返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中
- 成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁
// 调用该方法的前置条件是当前线程必须获取了锁
public final void signal() {
if (!isHeldExclusively()) // 当前线程必须是获取了锁的线程
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null) // 获取等待队列的首节点
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}