部分资源源自网络
一行一行源码分析清楚AbstractQueuedSynchronizer
Java并发之AQS详解 - waterystone - 博客园
AQS的经典使用案例推荐JDK中JUC组件源码:ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch等
ReentrantReadWriteLock源码:ReentrantReadWriteLock读写锁详解 - 平凡希 - 博客园
内部数据结构
图中展示AQS类较为重要的数据结构,包括int
类型变量state
用于记录锁的状态,继承自AbstractOwnableSynchronizer
类的Thread
类型变量exclusiveOwnerThread
用于指向当前排他的获取锁的线程,AbstractQueuedSynchronizer.Node
类型的变量head
及tail
。
其中Node
对象表示当前等待锁的节点,Node
中thread
变量指向等待的线程,waitStatus
表示当前等待节点状态,mode
为节点类型(共享、排他)。多个节点之间使用prev
及next
组成双向链表,参考CLH锁队列的方式进行锁的获取,但其中与CLH队列的重要区别在于CLH队列中后续节点需要自旋轮询前节点状态以确定前置节点是否已经释放锁,期间不释放CPU资源,而AQS
中Node
节点指向的线程在获取锁失败后调用LockSupport.park
函数使其进入挂起阻塞状态,让出CPU资源。故在前置节点释放锁时需要调用unparkSuccessor()->
LockSupport.unpark函数唤醒后继节点。
根据以上说明可得知此上图图主要表现当前thread0
线程获取了锁,thread1
线程正在等待。
Status
是一个
volatile int 变量,是控制线程状态的重要标志,一般通过CAS来保证线程安全的修改。不同AQS组件对status的使用和设计是不同的。
最简单的思路:status代表资源数。acquire取得资源需要status-1,若status不足则挂起。release是释放资源status+1,并唤醒队列中阻塞线程。排他模式下只有持有资源线程可以释放资源。
WaitStatus
4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。
-
CANCELLED:值为1,在同步队列中的线程等待超时或被中断,需要从同步队列中取消该Node的结点。进入该状态后的结点将不会再被阻塞。
-
SIGNAL:值为-1,当前节点的同步队列中后置节点是阻塞状态,当前节点释放资源或被取消时,必须unpark后置节点。
-
CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
-
PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
-
0状态:值为0,代表初始化状态。
保证挂起/唤醒线程并发安全思路
场景1:当前执行线程A为队列末尾线程,其执行业务逻辑结束后开始检查自身WaitStatus
状态判断是否存在后续节点需要唤醒,与此同时线程B入队挂起。
会不会出现线程A先检查WaitStatus
状态发现无后续节点,此时B挂起导致线程B永久无法唤醒?AQS如何保证线程A一定会唤醒线程B,避免线程B永久挂起?
线程A(末尾节点)结束:
CAS更新自身WaitStatus->唤醒后续节点
线程B挂起:
循环检查->CAS更新末尾节点(线程A节点)WaitStatus
状态: 见acquireQueued()
获取末尾节点->检查末尾节点状态->cas更新末尾节点状态->失败->循环
获取末尾节点->检查末尾节点状态->cas更新末尾节点状态->.........
两个线程都是通过先CAS成功,在进行后续操作(唤醒、挂起),避免并发问题。
流程图
排他模式
共享模式
组合模式
可以在一个AQS类中同时实现共享+排他模式,这样通过同一个队列和同一个AQS状态值state就可以同时控制两种模式线程节点,例如:ReentrantReadWriteLock。两种模式的release方法都会去同一个队列中唤醒thread节点。节点被唤醒后继续从其挂起位置执行代码。独占节点被唤醒后继续执行acquire(),共享节点被唤醒后继续执行acquireShared()。
源码分析
acquire获取排他资源、挂起当前线程
public final void acquire(int arg) {
if (!tryAcquire(arg) && //tryAcquire成功则不挂起线程,线程可以继续执行
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//由上面代码传入排他模式Node.EXCLUSIVE
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速放入队尾; 失败则执行enq(node)
Node pred = tail;
//当前存在队尾
if (pred != null) {
node.prev = pred;
//CAS放入队尾
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//CAS入队失败,执行enq(node)入队
enq(node);
return node;
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//自旋
for (;;) {
final Node p = node.predecessor();
//快速尝试
//如果当前node的前节点是head,且二次尝试tryAcquire成功
if (p == head && tryAcquire(arg)) {
//当前node设置为head,目的是减少线程挂起
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//上边if不成功
if (shouldParkAfterFailedAcquire(p, node) //判断前节点状态waitStatus,决定是否继续自旋
&& parkAndCheckInterrupt())//挂起线程park
interrupted = true;
//醒来后继续自旋
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//检查前节点状态,决定是park还是继续自旋
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) //-1
//前节点等待唤醒,则当前节点可以park
return true;
if (ws > 0) {
//前节点已取消
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//一直向前找到没取消的节点,修改其next为当前节点
pred.next = node;
} else {
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//继续自旋
return false;
}
//park挂起线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //线程在此挂起
return Thread.interrupted();//醒来后返回线程中断标志
}
唤醒下一个可用节点
public final boolean release(int arg) {
//tryRelease决定是否释放成功
//成功就尝试唤醒头节点的next
if (tryRelease(arg)) {
//头节点
Node h = head;
//首节点状态,唤醒下一节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
//此node为head
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* 尝试唤醒下一节点。但是下节点waitStatus是cancelled or apparently时,
* 从队尾tail向前,找到最前面的waitStatus <= 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);
}
简单的排它锁实现(非重入)
private class Sync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
assert arg == 1;
if (compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
assert arg == 1;
if (!isHeldExclusively()) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread()==Thread.currentThread();
}
}
AQS主要作用就是控制线程的挂起等待与唤醒,是设计线程阻塞组件的重要框架。其通过队列结构保存线程节点。head为当前持有资源节点,其释放资源时AQS组件会唤醒队列中下一个节点的线程。线程挂起后只能等待其他线程将其唤醒。
在并发环境下,加锁和解锁需要以下三个部件的协调:
- 锁状态。我们要知道锁是不是被别的线程占有了,这个就是 state 的作用,它为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state进行 +1 就可以,解锁就是减 1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对啊。然后唤醒等待队列中的第一个线程,让其来占有锁。
- 线程的阻塞和解除阻塞。AQS 中采用了 LockSupport.park(thread) 来挂起线程,head用 unpark 来唤醒下一节点线程。
- 阻塞队列。因为争抢锁的线程可能很多,但是只能有一个线程拿到锁,其他的线程都必须等待,这个时候就需要一个 queue 来管理这些线程,AQS 用的是一个 FIFO 的队列,就是一个链表,每个 node 都持有后继节点的引用。AQS 采用了 CLH 锁的变体来实现,感兴趣的读者可以参考这篇文章关于CLH的介绍,写得简单明了。
推荐阅读JDK中ReentrantLock、CountDownLatch源码理解AQS的使用。
他们都自己独立的 AbstractQueuedSynchronizer内部实现类Sync,控制线程的同步并发策略
AbstractQueuedSynchronizer中需要覆盖方法(钩子)
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要CAS控制共享资源 volatile state 的获取与释放方式即可,至于具体线程等待队列的维护(如线程获取资源失败入队/被唤醒出队等),AQS已经在顶层通过LockSupport实现挂起和唤醒。开发者只用关注获取和释放的逻辑即可。自定义同步器实现时主要实现以下几个可以覆盖的钩子方法:
- isHeldExclusively():boolean:该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):boolean:独占方式。尝试获取资源,成功则返回true。失败则返回false,加入阻塞队列挂起线程,等待被唤醒。
- tryRelease(int):boolean:独占方式。尝试释放资源,成功则返回true,唤醒后续等待结点。失败则返回false。
- tryAcquireShared(int):boolean:共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
共享模式线程取得资源后,若有剩余资源,会执行一次ReleaseShared尝试唤醒首节点后的线程节点。但是有一点要注意,唤醒只会按照同步队列顺序执行。比如:剩余资源为1,队列中[2]需要2,队列中[3]需要1。则不会跳过[2]去唤醒[3]。
- tryReleaseShared(int):boolean:共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
AbstractQueuedSynchronizer中提供的常用方法(非钩子)
acquire负责获取和挂起、release负责释放和唤醒
- getState()
- setState(int)
- compareAndSetState():boolean
- getExclusiveOwnerThread()
- setExclusiveOwnerThread()
- acquire(int):boolean //LockSupport.park实现thread挂起。内部是for循环,线程从挂起时执行到LockSupport.park位置被唤醒后,执行循环再次尝试获得资源,若失败会继续运行到LockSupport.park挂起。
- tryAcquireNanos(int arg, long nanosTimeout):boolean //等待获取资源,超时返回。其通过tryAcquire()方法判断资源,底层用LockSupport.parkNanos(Object blocker, long nanos)实现挂起超时返回
- acquireInterruptibly(int arg) //等待获取资源,中断立即抛出异常。
- release(int):boolean //释放+唤醒下一节点thread
LockSupport.park底层是响应Thread.interrupt方法的,当中断标志=true时,被LockSupport.park挂起的线程会被唤醒,继续执行代码。
acquireInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时Thread从LockSupport.park状态被唤醒,检查Thread.isInterrupted==true ,会立即抛出InterruptedException。
acquire方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则LockSupport.park继续休眠。只是在最后获取锁成功后再中断线程。
LockSupport的park、parkNanos 和unpark操作
原文:LockSupport的park和unpark操作_dump park-CSDN博客
其底层API是通过sun.misc.Unsafe实现。在java中一切线程都是Thread对象。
unpark(Thread thread) ; //给thread线程对象发放一次许可证。可以在park()之前执行。
park();//当前线程等待获取许可证。许可默认是被占用的,一直在这里等待unpark(当前thread)。
不可重发
以下代码主线程会卡死: unpark操作可以执行多次,但是最多释放一个许可证。但是park方法每次调用必须获取一个许可证才能继续执行,因此这段代码会一直等待。
public static void a2() {
Thread thread = Thread.currentThread();
LockSupport.unpark(thread);
LockSupport.unpark(thread); //可以多次释放一个许可证,多次执行也只会释放一个许可证。
LockSupport.park(); //通过,消费一个许可证
LockSupport.park(); //已无许可证,线程在这里等待unpark(thread)释放许可证
System.out.println("--");
}
改为以下代码主线程不会卡死:
public static void a2() {
Thread thread = Thread.currentThread();
LockSupport.unpark(thread);
LockSupport.park();
LockSupport.unpark(thread);
LockSupport.park();
System.out.println("--");
}
park可以响应中断
park操作等待获取许可证,可以被中断而继续执行,并且不会抛出异常信息。 InterruptedException由上层代码抛出。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("thread wait");
LockSupport.parkNanos(100000); //thread t 等待许可证、被中断、或自己醒来
System.out.println("thread wake.");
});
t.start();
Thread.sleep(1000);
System.out.println("main thread t.interrupt");
t.interrupt(); //如果因为park而被阻塞,可以响应中断请求,并且不会抛出InterruptedException。
System.out.println("main thread done.");
}
执行结果
thread wait
main thread t.interrupt
thread wake.
main thread done.
LockSupport实现Lock.Condition
ReentrantLock中的Condition就是直接由LockSupport实现的。Condition中维护一条先进先出队列,记录所有被当前condition挂起的线程。 await()底层直接调用LockSupport.park()且释放Lock,signal()底层直接调用LockSupport.unpark(firstThread)唤醒队列中第一个线程。因为操作Lock.condition前必须持有Lock所有保证了condition线程安全。