简介
AQS(AbstractQueuedSynchronizer):抽象的队列式同步器。是JUC下很多并发工具(ReentrantLock、CountDownLatch等)的基类;
AQS运用了模式设计方法,将可复用操作封装成模版(独占、共享),子类只需要关注 加锁、释放锁的逻辑即可。
基本组成
- state
- Node 组成的CLH 双向队列,FIFO。 简称:同步队列
- Node 组成的条件 单向队列。 简称:等待队列
state
描述:
表示资源状态,不同类表示的资源意义也不同;
volatile 修饰,CAS设置状态后,其它线程可以直接感知;
| 实现类 | 含义 | 说明 |
|---|---|---|
| ReentrantLock | 资源状态 | 0:资源没被获取,1:资源已被获取,>1:资源被同线程重入次数 |
| CountDownLatch | 未被释放的资源数 | 还有多少个资源没被释放 |
| Semaphore | 可获取的资源数量 | 还可以获取多少个资源 |
| ReentrantReadWriteLock | 读、写锁的状态 | 前16位表示读锁状态,后16位表示写锁状态 |
| LimitLatch ( Tomcat实现) | 未使用 | 使用自定义的count与limit进行比较 |
| CountDownLatch2 (RocketMq实现) | 未被释放的资源数 | 还有多少个资源没被释放; RebalanceImpl,默认是20s对消息队列与消息实例分配一次。先reset设为1,在await 20s,中途可以被mq中断立即进行分配 |
| ThreadPoolExecutor.Worker | 资源状态 | 0:资源未被获取,1:资源已被获取; 不可重入 |
Node
描述: Node中包含线程的引用,可以简单的把它想象成装线程的东西
字段:
| 状态名称 | 状态描述 |
|---|---|
| CANCELLED:1 | 已取消。是个异常状态 如代码执行异常,在catch中会将节点设为CANCELLED |
| SIGNAL:-1 | 当前节点执行完后会唤醒下个节点中的thread |
| CONDITION:-2 | 表示Node节点在等待队列中。Conditions时使用 |
| PROPAGATE:-3 | 共享模式下的状态。CountDownLatch时使用 |
| 0 | 默认状态 |
waiteState:节点等待状态 prev、next:上、下 一个Node节点引用
thread:线程引用
nextWaiter:
- 等待队列中表示下一个节点引用
- 同步队列中表示独占锁、共享锁标记【待补充】
同步队列
描述:
当线程获取资源失败或不满足获取条件后,将插入到同步队列尾部,并将前节点的waiteState设为SIGNAL,然后通过LockSupport#park挂起自己,等待被唤醒。
等待队列
ArrayBlockingQueue就借此实现的;
描述:
当线程执行AQS.ConditionObject#await之后,将插入到等待队列并从同步队列中删除节点,然后挂起当前线程
。。。阻塞中。。。
当别的线程执行AQS.ConditionObject#signal时,修改节点状态(-2 -> 0)后再次插入到同步队列,如果前一个节点状态不为SIGNAL或设置SIGNAL失败后将直接唤醒,否则会等到AQS#release时再唤醒
从阻塞中苏醒后,会再次尝试获取锁
ConditionObject
描述:
AQS中的内部类,包含 await、signal等方法,一个条件下对应一个等待队列
主要方法分析
独占模式
- acquire
- release
共享模式
-
acquireShare【待补充】
-
releaseShare【待补充】 子类为公平锁时使用
-
hasQueuedPredecessors
带计时的获取资源(CountDownLatch#await)
- tryAcquireNanos【待补充】
acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
以上代码可调整为
public final void acquire(int arg) {
// 调用子类实现类,尝试获取资源,获取成功直接返回
if(tryAcquire(arg)){
return;
}
// 创建Node节点,插入到同步队列尾部
Node node = addWaiter(Node.EXCLUSIVE);
// 从同步队列中获取锁,失败后会被挂起,线程阻塞
if(acquireQueued(node,arg)){
// 如果是被中断唤醒,则继续标记线程中断状态
selfInterrupt();
}
}
private Node addWaiter(Node mode) {
// 新建Node节点
Node node = new Node(Thread.currentThread(), mode);
// 因为等待队列是FIFO,所以选tail节点作为perv节点
Node pred = tail;
// 队列未初始化时,pred为null
if (pred != null) {
// 将尾节点赋值给当前节点的上一个节点
node.prev = pred;
// 尝试把队尾设为node,执行成功后 node = tail 。并不是把node赋值给pred
if (compareAndSetTail(pred, node)) {
// 将前一个尾节点的下一个节点设为当前节点
pred.next = node;
return node;
}
}
// 队列未初始化或上面的CAS失败进入
enq(node);
return node;
}
private final boolean compareAndSetTail(Node expect, Node update) {
// this: 包含要修改字段的对象
// tailOffset: 字段在对象内的偏移量,指的就是tail字段
// expect:字段的期望值
// update:如果该字段的值等于字段的期望值,用于更新字段的新值
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
// 如果addWaiter中的compareAndSetTail失败或队列需要初始化,会执行该方法
// 会返回当前节点的上一个节点
private Node enq(final Node node) {
// 自旋一直到入队成功
for (;;) {
Node t = tail;
if (t == null) { // CAS设置队头
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 将尾节点设置为当前节点的下一个节点
node.prev = t;
// CAS设置队尾
if (compareAndSetTail(t, node)) {
// 将前一个尾节点的下一个节点设为当前节点
t.next = node;
return t;
}
}
}
}
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)) {
// 将节点设置为头节点,清空Thread与prev
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 不是第一个节点或获取锁失败,判断是否挂起
if (shouldParkAfterFailedAcquire(p, node) &&
// 挂起当前线程,并且检测是否中断
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前一个节点等待状态为SIGNAL,代表当前节点可以被唤醒,可以去阻塞了
if (ws == Node.SIGNAL)
return true;
// 前一个节点状态为CANCELLED,则需要跳过,进行往前找
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// CAS将前一个节点等待状态改为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 线程挂起
LockSupport.park(this);
// 检测是否中断,消除中断标记,因为 除了被前一节点唤醒之外,还有可能被Thread#interrupt方法唤醒
// 如果不消除中断标记,再次获取锁失败后则无法进行阻塞
return Thread.interrupted();
}
release
public final boolean release(int arg) {
// 调用子类的实现类
if (tryRelease(arg)) {
Node h = head;
// 头节点不为空且状态不是0,进行唤醒操作
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 将头节点的状态更新为0
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 若后继结点为空,或状态为CANCEL,则从后尾部往前遍历找到最前的一个处于正常阻塞状态的结点
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)
// 唤醒下一个节点,下一个节点将从 #acquireQueued中的#parkAndCheckInterrupt方法醒来
LockSupport.unpark(s.thread);
}
hasQueuedPredecessors
// true:自己前面还有节点 false: 说明自己就是第一个节点
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 获取头节点的下一个节点,并且 下一个节点中的线程 != 当前线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
acquireShare
/**
* 共享式获取资源的模版方法
*/
public final void acquireShared(int arg){
// 调用子类实现,<0代表未获取到资源
if (tryAcquireShared(arg) < 0)
// 未获取到资源,入队
doAcquireSharedInterruptibly(arg);
}
问题
- #relase#unparkSuccessor,如果头节点的下个节点为空或等待状态>0,为什么会从尾节点开始找,不是破坏同步队列了吗?公平锁不会变的不公平吗?
并发情况下,enq方法可能会导致,某节点下的next节点为null,但该节点确是后续节点的prev节点。
详情可以参照: www.tqwba.com/x_d/jishu/2…
- 在AQS#parkAndCheckInterrupt中,LockSupport#park之后要Thread#interrupted方法?
Thread#interrupt方法也会唤醒LockSupport#park,如果不消除中断状态,CAS再次获取锁失败后将无法阻塞。
详情可参照:LockSupport
-
AQS的等待队列跟CLH比有哪些区别
- CLH是单向队列,AQS等待队列是双向的
- CLH是自旋查看上一个节点的lock状态,AQS等待队列也有状态是waitStatus,但不会一直自旋查看上一个节点,而是自旋一段时间符合条件后阻塞,让出cpu的时间片,等待上一个节点唤醒
-
ConditionObject#await、#signal与Object#wait、#notify区别
【待补充】
- 独占模式、共享模式区别
-
独占模式,资源只能被一个线程占有(ReentrantLock)。共享模式,资源可以被多个线程同时占有(CountDownLatch、Semaphore)。
-
AQS子类#tryAcquire返回值不同,独占返回boolean,true代表获取成功,共享返回int,>=0代表成功
-
独占模式在tryAcquire、tryRelease修改AQS.state状态时不需要考虑多线程情况,而共享模式则需要考虑,像CountDownLatch、Semaphore都使用自旋+CAS解决
-