1. 前言
废话不多说,直入主题。
在JUC(java.lang.concurrent包)中,有成吨的好用的并发控制工具,能实现不同的功能,帮助我们解决不同程度的问题。比如:
- 重入锁:java.lang.concurrent.locks.ReentrantLock,功能类似synchronized,更加灵活
- 读写锁:java.lang.concurrent.locks.ReentrantReadWriteLock,支持读写锁分离,读锁为共享锁,支持不同线程同时持有锁资源,写锁与其他写锁和读锁都互斥,属于独占资源,可解决“读时共享不阻塞,写时阻塞更新”的功能。
- CountDownLatch:多线程倒计数阻塞器,支持主线程等待所有子线程完成某一项任务并倒计数后,主线程再从阻塞状态唤醒执行后续业务,适用于主线程等待获取多个子线程执行结果后的应用场景
- Semaphore:信号量控制器,支持指定信号量为N后,同一时间只允许有不超过N的线程获得执行权限,后续线程无法获得执行权限。
这些工具能够实现不同的各种各样的功能。虽然在功能和实现上都不一样,但他们的能力都源于一处,那就是老生常谈的AQS。
所谓万变不离其宗,水之呼吸也好,风之呼吸也好,本质上都是日志呼吸。
1.1 AQS是个什么东西呢
AQS其实是我们对它的一个简称,人家全名叫做:AbstractQueuedSynchronizer,翻译过来就是抽象的基于队列的同步器
它提供的功能,从字面意思上就能够略窥一二,无非就是用来实现锁嘛,多线程竞争嘛,多线程来争抢资源时,只允许一个线程获得权限,没获得权限的就阻塞,获得了锁的线程释放锁之后,就唤醒被阻塞的线程,仅此而已。
那如何实现呢?基于队列呗,人家都说了,是基于基于队列的同步器。但是光看这个名字还是挺抽象的,不过话都说到这了,大家作为有经验的开发人员,我们理解和学习一个知识的时候,最好要先去学会猜测,然后通过看文档和源码来一步步印证自己的猜想,比如,在你不知道有AQS的情况下,假如你是Duag Lee,你将如何设计呢?
面试中,也经常被面试官问到这个问题,aqs不管在日常开发工作中,还是在决定我们面试是否通过的层面,都是极为重要的。
2. 思考如果让你来设计aqs的核心功能,你将如何设计
如果是我,我肯定想,都基于队列了,那队列是干嘛的,用来排队的呗,排啥队呢?那必然是没有抢到锁的要排队等待呗,所以这个队列,就是为了给没有抢到锁的去等待的。
假如有一段代码,业务上可能存在多线程并行调用的场景,此时有5个请求,每个请求是一个独立的线程,此时都来调这段代码,但在调用之前,需要先争抢同步锁,毕竟这段代码要保证线程安全,只能同时被一个线程调用,所以它必须满足以下特征或条件:
- 首先抢到锁的获得执行权,返回true
- 后续没有抢到的,不管多少线程,统统排队等待
- 抢到锁的释放锁之后,依次唤醒其它处于等待队列的线程
- 将上述步骤一直循环
好了,看上去能够满足我们的需求了,再画个图简单表示一下:
差不多就是这个意思。
但这还不够,里面其实有很多可以优化的点,或者说需要解决的问题,比如:
- 在AQS还没有现成占用时,刚开始的并发竞争如何解决
- 线程的阻塞和唤醒如何实现
- 处于同步队列中的线程的唤醒策略是什么,结构如何实现,要有哪些元素
- 获得AQS执行权限的线程在释放之前想要等待如何实现
- ...
似乎这并不是一件容易的事情,上面我们自行推测并实现的看来是一个只有部分核心功能的,充满bug的版本,当然,一款好用的工具并不是一朝一夕就实现的出来的,好在我们有了一个初步的猜想,有了这个基础,接下来可以去研究一下真正的AQS是如何实现的
2.1 一开始如何抢占资源呢
有经验的开发者很容易就能想到使用CAS,其实使用什么并不重要,重要的是思想,无非就是要保证原子性嘛,多个线程都同时去抢占资源,只要在一个线程操作时,阻塞其它线程就可以了,所以CAS是最佳选择。
思想确定了,那么实现是如何做的呢?
先直接看看AQS的实现吧:
// 初始化UNSAFE对象
private static final Unsafe U = Unsafe.getUnsafe();
// 获取字段在直接内存中相对当前对象的偏移量
private static final long STATE
= U.objectFieldOffset(AbstractQueuedSynchronizer.class, "state");
// 基于CAS修改state变量
protected final boolean compareAndSetState(int expect, int update) {
return U.compareAndSetInt(this, STATE, expect, update);
}
没错,肯定得是CAS,这里也直接用到了Java中的UNSAFE类来进行cas操作,目的就是能够基于当前对象,在直接内存中基于相对偏移量去修改数据。
但是,只有这一种方式吗,AQS中是基于state变量cas修改去处理刚开始的并发安全问题的,AQS里面在成功修改了state变量后,还会将当前抢占到资源的线程,设置到exclusiveOwnerThread中
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
那,能不能不要state变量,直接基于CAS去设置这个exclusiveOwnerThread呢?比如,利用Java中的原子类AtomicReference来进行实现
伪代码如下:
public class AtomicReferenceTest {
private AtomicReference<Thread> ar = new AtomicReference<>();
public void atomicReference() {
if(ar.compareAndSet(null, Thread.currentThread())) {
// do your business
} else {
// 失败,加入同步队列
}
}
}
这样能够满足我们最原始的需求,那为什么AQS还要用state呢,自然有他的用处,因为AQS是一个基础的同步器工具,很多上层工具比如ReentrantLock,ReentrantReadWriteLock等都是基于它来实现的,需要分别用到state和exclusiveOwnerThread,这里不进行展开,只是为了拓展大家的思考方式,要思考为什么要这样做,如果用另外的方式做是否可以。
2.2 抢占失败如何进行阻塞等待呢
首先要思考,为什么抢占失败的要基于队列来阻塞呢?不基于队列,直接阻塞行不行,当然有效果,但是不要忘了,我们除了阻塞以外,抢到执行权的线程之外完业务代码,释放资源后,要去唤醒,那我去哪里找被阻塞的这些线程呢?在一个池子里找吗?
那就得维护一个池子,最合理的选择就是一个Set集合,但是我要唤醒哪一个呢,看来只能随机了。不是不行,但仔细想想,好像有失公平,如果我一个比较重要的业务线程第一个被阻塞,但由于后面不断有现成进来被阻塞,由于随机算法的阻碍,我一开始那个重要的线程迟迟无法被唤醒,岂不是误了大事。
其实在生活中也是,很多人去窗口办事,办事处刚开门的时候,肯定要迎来一波疯抢,但一旦第一个人抢到了位置开始办事了,后面的人就只能等着,那怎么等呢,不用说,肯定是排队,先来先排,所以程序的逻辑设计也是源于生活。
所以就有了我们的同步队列,这样我在获取到资源的线程释放资源后,就能够根据这个队列,来进行优先级控制了,丝毫不混乱。
AQS中,是通过一个Node对象来封装,然后组成一个双向链表完成的同步队列,同时存在head头结点和tail尾节点,AQS只需要关注这两个节点,并随着阻塞线程唤醒动态调整head和tail的指向即可轻松完成调度任务
由于本文大多数只谈思想,这里就不单独贴源码了,贴了也是占篇幅,没啥实际意义
2.3 如何实现线程通信唤醒阻塞中的线程呢
前面提到,链表是基于Node节点来封装的,链表的指向实际上是Node对象。
Node对象定义如下:
abstract static class Node {
volatile Node prev; // initially attached via casTail
volatile Node next; // visibly nonnull when signallable
Thread waiter; // visibly nonnull when enqueued
volatile int status; // written by owner, atomic bit ops by others
// methods for atomic operations
final boolean casPrev(Node c, Node v) { // for cleanQueue
return U.weakCompareAndSetReference(this, PREV, c, v);
}
final boolean casNext(Node c, Node v) { // for cleanQueue
return U.weakCompareAndSetReference(this, NEXT, c, v);
}
final int getAndUnsetStatus(int v) { // for signalling
return U.getAndBitwiseAndInt(this, STATUS, ~v);
}
final void setPrevRelaxed(Node p) { // for off-queue assignment
U.putReference(this, PREV, p);
}
final void setStatusRelaxed(int s) { // for off-queue assignment
U.putInt(this, STATUS, s);
}
final void clearStatus() { // for reducing unneeded signals
U.putIntOpaque(this, STATUS, 0);
}
private static final long STATUS
= U.objectFieldOffset(Node.class, "status");
private static final long NEXT
= U.objectFieldOffset(Node.class, "next");
private static final long PREV
= U.objectFieldOffset(Node.class, "prev");
}
里面除了prev和next变量来完成双向链表的功能以外,还有一个waiter变量,这是一个Thread对象,也就是说,获得资源的线程能够基于head或者tail获取Node中的Thread,既然拿到了Thread,自然就能够轻易的通过api去唤醒和阻塞了
通过LockSupport类的park实现阻塞,unpark实现唤醒,本质还是基于UNSAFE类来进行操作,不贴源码水文章了,有兴趣自行看源码
2.4 被唤醒的线程如何继续抢占呢
首先要思考一个问题,唤醒的线程再去抢占,还需要经过CAS吗,有必要吗?
乍一看,我基于队列依次唤醒,每次只唤醒一个,是不是就直接给资源了,不用再重新去CAS了呢?好像是,但是真的是这样吗?有没有啥问题呢?好像没问题,真的没问题吗,暂时想不到有啥问题,那不如先来看源码
public final boolean release(int arg) {
if (tryRelease(arg)) {
signalNext(head);
return true;
}
return false;
}
private static void signalNext(Node h) {
Node s;
if (h != null && (s = h.next) != null && s.status != 0) {
s.getAndUnsetStatus(WAITING);
LockSupport.unpark(s.waiter);
}
}
哦!没想到真的就是这样,真的不需要再去CAS了,就是基于head去遍历,依次唤醒,但!唤醒之后就是直接抢占到资源了吗?再来看,要看一开始这些鲜橙阻塞到哪里,被唤醒后才会从下面的地方开始执行逻辑
final int acquire(Node node, int arg, boolean shared,
boolean interruptible, boolean timed, long time) {
Thread current = Thread.currentThread();
byte spins = 0, postSpins = 0; // retries upon unpark of first thread
boolean interrupted = false, first = false;
Node pred = null; // predecessor of node when enqueued
/*
* Repeatedly:
* Check if node now first
* if so, ensure head stable, else ensure valid predecessor
* if node is first or not yet enqueued, try acquiring
* else if node not yet created, create it
* else if not yet enqueued, try once to enqueue
* else if woken from park, retry (up to postSpins times)
* else if WAITING status not set, set and retry
* else park and clear WAITING status, and check cancellation
*/
for (;;) {
if (!first && (pred = (node == null) ? null : node.prev) != null &&
!(first = (head == pred))) {
if (pred.status < 0) {
cleanQueue(); // predecessor cancelled
continue;
} else if (pred.prev == null) {
Thread.onSpinWait(); // ensure serialization
continue;
}
}
if (first || pred == null) {
boolean acquired;
try {
if (shared)
acquired = (tryAcquireShared(arg) >= 0);
else
acquired = tryAcquire(arg);
} catch (Throwable ex) {
cancelAcquire(node, interrupted, false);
throw ex;
}
if (acquired) {
if (first) {
node.prev = null;
head = node;
pred.next = null;
node.waiter = null;
if (shared)
signalNextIfShared(node);
if (interrupted)
current.interrupt();
}
return 1;
}
}
if (node == null) { // allocate; retry before enqueue
if (shared)
node = new SharedNode();
else
node = new ExclusiveNode();
} else if (pred == null) { // try to enqueue
node.waiter = current;
Node t = tail;
node.setPrevRelaxed(t); // avoid unnecessary fence
if (t == null)
tryInitializeHead();
else if (!casTail(t, node))
node.setPrevRelaxed(null); // back out
else
t.next = node;
} else if (first && spins != 0) {
--spins; // reduce unfairness on rewaits
Thread.onSpinWait();
} else if (node.status == 0) {
node.status = WAITING; // enable signal and recheck
} else {
long nanos;
spins = postSpins = (byte)((postSpins << 1) | 1);
if (!timed)
LockSupport.park(this);
else if ((nanos = time - System.nanoTime()) > 0L)
LockSupport.parkNanos(this, nanos);
else
break;
node.clearStatus();
if ((interrupted |= Thread.interrupted()) && interruptible)
break;
}
}
return cancelAcquire(node, interrupted, interruptible);
}
这段代码有点长,但是是AQS中获取资源的核心代码,其中有一个LockSupport.park方法,这个就是阻塞这些线程的地方,所以被唤醒的线程将要从LockSupport.park方法下方开始再执行,也就是说会再去抢占资源,因为外层是一个for自旋逻辑。
再次抢占,需要去检测AQS中的exclusiveOwnerThread是否与当前线程一致,看看state是否为0等,否则将再次进入同步队列等待。
2.5 假如抢占到锁的线程暂时需要等待如何实现呢
对标jdk中synchronized关键字中的wait,AQS中也有一个wait的功能,也就是抢占到资源的线程,在真正地释放资源之前,如果需要临时让出资源,则能够基于另外一个Condition条件队列进行等待。
来看一下AQS中的await函数源码
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
ConditionNode node = new ConditionNode();
int savedState = enableWait(node);
LockSupport.setCurrentBlocker(this); // for back-compatibility
boolean interrupted = false, cancelled = false, rejected = false;
while (!canReacquire(node)) {
if (interrupted |= Thread.interrupted()) {
if (cancelled = (node.getAndUnsetStatus(COND) & COND) != 0)
break; // else interrupted after signal
} else if ((node.status & COND) != 0) {
try {
if (rejected)
node.block();
else
ForkJoinPool.managedBlock(node);
} catch (RejectedExecutionException ex) {
rejected = true;
} catch (InterruptedException ie) {
interrupted = true;
}
} else
Thread.onSpinWait(); // awoke while enqueuing
}
LockSupport.setCurrentBlocker(null);
node.clearStatus();
acquire(node, savedState, false, false, false, 0L);
if (interrupted) {
if (cancelled) {
unlinkCancelledWaiters(node);
throw new InterruptedException();
}
Thread.currentThread().interrupt();
}
}
源码解释起来比较枯燥,无非就是,在调用await函数等待时,进行一波校验,看当前线程是否是获取到资源的线程,是否在同步等待队列中,然后仍然通过LockSupport.park阻塞当前线程,并添加到Condition队列中,大概就是像下面这幅图一样
图中上面是同步队列,下面是Condition队列,这是一个单向链表。
当然,有阻塞就有唤醒,从Condition队列中唤醒的函数,是AQS中的signal函数,对标synchronized中notify函数,当然synchronized中还有个notifyAll,所以AQS中也有一个signalAll
public final void signal() {
ConditionNode first = firstWaiter;
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
if (first != null)
doSignal(first, false);
}
public final void signalAll() {
ConditionNode first = firstWaiter;
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
if (first != null)
doSignal(first, true);
}
private void doSignal(ConditionNode first, boolean all) {
while (first != null) {
ConditionNode next = first.nextWaiter;
if ((firstWaiter = next) == null)
lastWaiter = null;
if ((first.getAndUnsetStatus(COND) & COND) != 0) {
enqueue(first);
if (!all)
break;
}
first = next;
}
}
源码也很容易看懂,无非就是校验一波线程是否匹配,然后从Condition队列中依次唤醒或全部唤醒
3. 总结
AQS是Java并发控制工具中的最核心抽象类,只有懂的其中的实现原理,才能更好的面试和开发,解决遇到的问题。
本文旨在帮助大家理解AQS的核心设计思想,以及为什么要这样去设计。我们在理解掌握一项技术的时候,初学者是学习使用方法,进阶者会去理解其中的原理,高手会去思考为什么要这样去写去设计,并结合自身的经验去理解,融入自己的东西,变成自己的知识。