深入理解 synchronized 关键字(关于对象头和锁的升级)
深入理解 volatile 关键字(内存屏障和指令重排问题)
AQS
1. AQS 概述
AQS是 AbstractQueuedSynchronizer 的简称,即 抽象队列同步器 ,从字⾯意思上理解:
- 抽象:抽象类,只实现⼀些主要逻辑,有些⽅法由⼦类实现;
- 队列:使⽤先进先出(FIFO)队列存储数据;
- 同步:实现了同步的功能。
那AQS有什么⽤呢?AQS是⼀个⽤来构建锁和同步器的框架,使⽤AQS能简单且⾼效地构造出应⽤⼴泛的同步器,⽐如我们提到的ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask
等等皆是基于AQS的。
当然,我们⾃⼰也能利⽤AQS⾮常轻松容易地构造出符合我们⾃⼰需求的同步器,只要子类实现它的⼏个 protected ⽅法就可以了,在下⽂会有详细的介绍。
2. AQS 的数据结构
AQS内部使⽤了⼀个volatile的变量state来作为资源的标识。同时定义了⼏个获取和改版state的protected⽅法,⼦类可以覆盖这些⽅法来实现⾃⼰的逻辑:
getState()
setState()
compareAndSetState()
这三种叫做均是原⼦操作,其中compareAndSetState的实现依赖于Unsafe的
compareAndSwapInt()⽅法。
⽽AQS类本身实现的是⼀些排队和阻塞的机制,⽐如具体线程等待队列的维护(如获取资源失败⼊队/唤醒出队等)。它内部使⽤了⼀个先进先出(FIFO)的双端队列,并使⽤了两个指针head和tail⽤于标识队列的头部和尾部。其数据结构如图:
3. 资源共享模式
资源有两种共享模式,或者说两种同步⽅式:
独占模式(Exclusive):资源是独占的,⼀次只能⼀个线程获取。如ReentrantLock。
共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。如Semaphore/CountDownLatch。
⼀般情况下,⼦类只需要根据需求实现其中⼀种模式,当然也有同时实现两种模式的同步类,如 ReadWriteLock
。
AQS中关于这两种资源共享模式的定义源码(均在内部类Node中)。我们来看看Node的结构:
static final class Node {
// 标记⼀个结点(对应的线程)在共享模式下等待
static final Node SHARED = new Node();
// 标记⼀个结点(对应的线程)在独占模式下等待
static final Node EXCLUSIVE = null;
// waitStatus的值,表示该结点(对应的线程)已被取消
static final int CANCELLED = 1;
// waitStatus的值,表示后继结点(对应的线程)需要被唤醒
static final int SIGNAL = -1;
// waitStatus的值,表示该结点(对应的线程)在等待某⼀条件
static final int CONDITION = -2;
/*waitStatus的值,表示有资源可⽤,新head结点需要继续唤醒后继结点(共享模式下,多线程并发执行*/
static final int PROPAGATE = -3;
// 等待状态,取值范围,-3,-2,-1,0,1
volatile int waitStatus;
volatile Node prev; // 前驱结点
volatile Node next; // 后继结点
volatile Thread thread; // 结点对应的线程
Node nextWaiter; // 等待队列⾥下⼀个等待条件的结点
// 判断共享模式的⽅法
final boolean isShared() {
return nextWaiter == SHARED;
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
// 其它⽅法忽略,可以参考具体的源码
}
// AQS⾥⾯的addWaiter私有⽅法
private Node addWaiter(Node mode) {
// 使⽤了Node的这个构造函数
Node node = new Node(Thread.currentThread(), mode);
// 其它代码省略
}
注意:通过Node我们可以实现两个队列,⼀是通过prev和next实现CLH队列
(线程同步队列,双向队列),⼆是nextWaiter实现Condition条件上的等待线程
队列(单向队列),这个Condition主要⽤在ReentrantLock类中。
4. AQS 的主要源码解析
AQS的设计是基于模板⽅法模式的,它有⼀些⽅法必须要⼦类去实现的,它们主要有:
- isHeldExclusively():该线程是否正在独占资源。只有⽤到condition才需要去实现它。
- tryAcquire(int):独占⽅式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占⽅式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享⽅式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可⽤资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享⽅式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
这些⽅法虽然都是 protected ⽅法,但是它们并没有在AQS具体实现,⽽是直接抛出异常(虽然不知道这⾥为什么不使⽤抽象⽅法的实现⽅式)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
⽽AQS实现了⼀系列主要的逻辑。下⾯我们从源码来分析⼀下获取和释放资源的主要逻辑:
4.1 获取资源
获取资源的⼊⼝是acquire(int arg)⽅法。arg是要获取的资源的个数,在独占模式下始终为1。我们先来看看这个⽅法的逻辑:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在上面我们知道这里的 tryAcquire(arg)
方法是由子类来进行实现的,如果获取资源失败,下一步要执行的方法就是 addWaiter(Node.EXCUSIVE,arg)
传入的参数表示其是独占的。
我们跳到addWaiter()
这个方法中去
private Node addWaiter(Node mode) {
// 这里首先新建了一个Node,保存当前执行的线程。
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 这里进行判断队列尾部是否为 null 如果是,
// 表示队列为空,执行enq(node) 方法
Node pred = tail;
// 如果队列不为空,则将创建的node 插入到队列尾部
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 并没有接收返回值
enq(node);
// 返回插入的节点
return node;
}
我们能够很容易的看出这个方法的逻辑,所以我们进入到 enq(node) 方法中去。
enq(node) 方法
private Node enq(final Node node) {
// 由于是使用了 CAS 方法插入,所以死循环,直到将节点插入队列为止
for (;;) {
Node t = tail;
// 如果尾结点为 null 就新建一个头结点,里面没有线程任务
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
}
// 有了头结点之后,就利用CAS来进行插入操作
else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
// 这里虽然返回的是 t (head 节点)但是是返回
// 到addWaiter方法中去,那边并没有接收这个返回值
// 这个返回值在其他地方可能有用。最终addWaiter返回
// 的节点就是当前插入到队列尾部的节点
return t;
}
}
}
}
在这个方法中,我们需要注意的是,使用一个for 死循环,如果当前的节点为null 且没有头结点时,就会创建一个head节点来当做头结点,然后进入下一次的for循环,此时就有头结点了,就会进入else 逻辑,使用CAS 来进行插入操作,直到将节点插入到队列为止,才进行返回
OK,现在回到最开始的aquire(int arg)⽅法。现在通过addWaiter⽅法,已经把⼀个Node放到等待队列尾部了。⽽处于等待队列的结点是从头结点⼀个⼀个去获取资源的。具体的实现我们来看看acquireQueued⽅法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 拿到 node 的前驱节点
final Node p = node.predecessor();
// 如果 p 是头结点,并且尝试获取资源成功
if (p == head && tryAcquire(arg)) {
// 表示当前线程得到了资源开始执行,
// 将当前节点变为头结点
setHead(node);
// 将上一个头结点置为 null 有助于垃圾回收
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
如同上面代码的注释一样,传入的节点会先进行判断,其前驱节点是否为头结点,如果是,则尝试获取资源,如果获取成功,代表这个线程能够开始运行,于是将此节点置为头结点,将之前的头结点置为 null。
我们继续往下看
shouldParkAfterFailedAcquire
方法
// pred 表示前驱节点,node 表示当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 如果等于 Node.SIGNAL 表示该线程需要 signal 来继续,所以能够休息,返回true
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 如果 ws > 0 则表示这个线程已经被取消
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这个函数也比较简单,判断这个线程是否需要Signal 或者是否已经被取消。都不是就用CAS 来请求资源,最终返回false。
这⾥parkAndCheckInterrupt⽅法内部使⽤到了LockSupport.park(this),顺便
简单介绍⼀下park。
LockSupport类是Java 6 引⼊的⼀个类,提供了基本的线程同步原语。
LockSupport实际上是调⽤了Unsafe类⾥的函数,归结到Unsafe⾥,只有两个函数:
park(boolean isAbsolute, long time):阻塞当前线程
unpark(Thread jthread):使给定的线程停⽌阻塞
所以结点进⼊等待队列后,是调⽤park使它进⼊阻塞状态的。只有头结点的线程是处于活跃状态的。
当然,获取资源的⽅法除了acquire外,还有以下三个:
- acquireInterruptibly:申请可中断的资源(独占模式)
- acquireShared:申请共享模式的资源
- acquireSharedInterruptibly:申请可中断的资源(共享模式)
可中断的意思是,在线程中断时可能会抛出 InterruptedException
总结起来的⼀个流程图:
4.2 释放资源
释放资源相⽐于获取资源来说,会简单许多。在AQS中只有⼀⼩段实现。源码:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
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)
LockSupport.unpark(s.thread);
}