文章目录
AQS简介
AQS (AbstractQueuedSynchronizer
)是一个用来构建锁和同步器的框架。
- 抽象:抽象类,只实现一些主要逻辑,有些方法由子类实现;
- 队列:使用先进先出(FIFO)队列存储数据;
- 同步:实现了同步的功能。
它是一个抽象类,提供了构建同步器的基础功能,子类可以通过实现一些关键的protected
方法来创建自定义的同步器。
AQS 主要用于实现各种同步器,如:
ReentrantLock
Semaphore
ReentrantReadWriteLock
SynchronousQueue
FutureTask
通过AQS,开发者能够高效地构建各种同步器,满足不同的需求。
AQS的数据结构
AQS的核心数据结构包括:
state变量
-
state
变量 :用于标识资源的状态。它是一个volatile
类型的整型变量。AQS通过以下方法来操作state
变量:javagetState() setState() compareAndSetState()
这些方法都是原子操作,其中compareAndSetState
使用了Unsafe
的compareAndSwapInt()
方法来保证操作的原子性。
等待队列
- 等待队列 :AQS使用一个FIFO队列来管理线程的排队和阻塞。这个队列实际上存储的是
Node
节点,而不是线程对象。每个Node
节点包含以下信息:
java
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结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的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);
// 其它代码省略
}
SHARED
和EXCLUSIVE
:分别表示共享模式和独占模式的标记。waitStatus
:表示节点的等待状态。它可以是CANCELLED
(已取消)、SIGNAL
(需要唤醒后继节点)、CONDITION
(等待条件)或PROPAGATE
(需要继续唤醒后继节点)。
Node
节点通过prev
和next
实现双向队列,支持线程的排队。而通过nextWaiter
实现条件队列,主要用于Condition
的等待线程。
资源共享模式
AQS支持两种资源共享模式:
-
独占模式(Exclusive):
- 资源是独占的,一次只能由一个线程获取。例如:
ReentrantLock
。
- 资源是独占的,一次只能由一个线程获取。例如:
-
共享模式(Share):
- 资源可以被多个线程同时获取,具体的资源个数可以通过参数指定。例如:
Semaphore
、CountDownLatch
。
子类通常只需实现其中一种模式的逻辑。但也有同步类同时实现两种模式,如
ReadWriteLock
。 - 资源可以被多个线程同时获取,具体的资源个数可以通过参数指定。例如:
AQS的主要方法源码解析
AQS的设计基于模板方法模式,提供了一些必须由子类实现的方法。这些方法包括:
isHeldExclusively()
:检查当前线程是否独占了资源。这通常与条件变量的使用有关。tryAcquire(int arg)
:尝试以独占模式获取资源。tryRelease(int arg)
:尝试释放资源(独占模式)。tryAcquireShared(int arg)
:尝试以共享模式获取资源。tryReleaseShared(int arg)
:尝试释放资源(共享模式)。
这些方法虽然是protected
的,但AQS本身并不提供具体的实现,而是抛出UnsupportedOperationException
。子类需要实现这些方法以定义具体的资源获取和释放逻辑。例如:
java
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
获取资源
获取资源的入口方法是acquire(int arg)
:
java
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)
:尝试获取资源。如果获取失败,则将当前线程加入等待队列。addWaiter(Node.EXCLUSIVE)
:将线程封装成Node
节点并添加到等待队列的尾部。
addWaiter
方法源码如下:
java
private Node addWaiter(Node mode) {
// 生成该线程对应的Node节点
Node node = new Node(Thread.currentThread(), mode);
// 将Node插入队列中
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 使用CAS尝试,如果成功就返回
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果等待队列为空或者上述CAS失败,再自旋CAS插入
enq(node);
return node;
}
// 自旋CAS插入等待队列
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
插入等待队列 :addWaiter
方法通过compareAndSetTail
和enq
方法将节点插入队列尾部。enq
方法使用自旋CAS确保线程安全。
获取资源的核心逻辑在acquireQueued
方法中:
java
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
final Node p = node.predecessor();
// 如果node的前驱结点p是head,表示node是第二个结点,就可以尝试去获取资源了
if (p == head && tryAcquire(arg)) {
// 拿到资源后,将head指向该结点。
// 所以head所指的结点,就是当前获取到资源的那个结点或null。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果自己可以休息了,就进入waiting状态,直到被unpark()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
node.predecessor()
:获取节点的前驱节点。tryAcquire(arg)
:尝试获取资源。setHead(node)
:设置新的头节点。parkAndCheckInterrupt()
:将当前线程挂起,直到被唤醒。
这里parkAndCheckInterrupt方法内部使用到了LockSupport.park(this),
LockSupport类是Java 6 引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:
park(boolean isAbsolute, long time):阻塞当前线程unpark(Thread jthread):使给定的线程停止阻塞
释放资源
释放资源的方法是release(int arg)
:
java
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) {
// 如果状态是负数,尝试把它设置为0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 得到头结点的后继结点head.next
Node s = node.next;
// 如果这个后继结点为空或者状态大于0
// 通过前面的定义我们知道,大于0只有一种可能,就是这个结点已被取消
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);
}
tryRelease(arg)
:尝试释放资源。unparkSuccessor(node)
:唤醒等待队列中的下一个线程。