背景
AbstractQueuedSynchronizer 简称 AQS 是 JDK 中 JUC 包的核心方法。包括 ReentrantLock , Semaphore , CountDownLoatch 甚至是 ThreadPoolExectur 中的 Worker 都是基于其实现的。可以说搞明白了 AQS 就搞懂了 JUC。
AQS 做了什么
AQS 提供了一个基于 FIFO 的快速实现阻塞锁的框架。
为什么要 AQS
我们知道在 Java 中有一个关键字 synchronized 已经用于同步,并且随着 JDK 的不断迭代, synchronized 已经拥有了较好的性能,既然这样,为什么还会需要 AQS 呢。
synchronized 的局限性
虽然 synchronized 已经能有很好性能的,但是 synchronized 底层其实还是基于一个 CAS 的自旋操作,也就是说 synchronized 其实是个自旋锁。当处于高并发的环境之中,这会导致极强的锁竞争,大量的自旋操作,会极大的浪费计算资源。所以 synchronized 在高并发的场景并不是特别好。
注: 虽然我们知道 synchronized 做了很多优化,但是那些偏向锁,轻量级锁的优化,归根结底都是加速当前
并发量较小,我们该怎么设计,让锁占用更少的系统资源。相关文章参考 [Java 并发编程: 核心理论]
(http://www.cnblogs.com/paddix/p/5374810.html)
AQS 的优势
既然讲到了 synchronized 的局限性,自然要讲讲 AQS 的优势。 AQS 的主要优势有两点
一: 提供更丰富的 API
基于 AQS 的 ReentrantLock, Semaphore 等相比于 synchronized 提供了更为丰富的 API 接口,也就是提供了更为丰富更强大的功能。
二: 挂起线程,更适合高并发场景
AQS 内部改造的 CLH 队列,在一定条件下会将没有获取锁的线程挂起,等到锁被释放后再唤起,这样就保证了在高并发的场景下,不会有太多的线程竞争资源,而是大部分都在等待被唤醒,减少了性能的损耗。
AQS 实现原理
AQS 的实现,主要依赖两个部分,第一个改造的 CLH 队列,另一个是 LockSupport 线程挂起方法。首先我们介绍一下标准的 CLH 队列,然后我们再聊 AQS 中改造的 CLH 队列。
CLH 队列
CLH 队列其实很简单,CLH 队列其实就是一个链表,链表的每个节点有一个 isLocked
字段,该字段标明当前是否需要持有锁。以及一个 prev
指针,指向前置节点。
CLH 入队列获取锁
当一个线程要获取锁的时候,会先入队列尾部,然后读取前置节点的 isLocked 字段,如果该字段为 false ,则不断自旋读取 isLocked 字段,直到 isLocked 为 true,则表示当前线程获取锁成功。
CLH 出队列释放锁
当线程释放锁之后,会将自身的节点出队列,然后将自身节点的 isLocked 字段置为 true,让后继节点获取锁。
以上就是标准 CLH 队列的实现方式。
LockSupport 方法
标准 CLH 队列实现非常简单, 但是仔细一看会发现,其实 CLH 队列也是一个自旋方式控制获取锁同步的方案。如果直接使用 CLH 队列其实和 synchronized 没有太大的区别。所以这个时候 LockSupport 出场了!!!当!当!当!当!
LockSupport 内部有两个关键方法,分别为
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
其中 park 方法用于挂起线程,这样除非线程重新被唤醒,否则线程将不会再占用操作系统计算资源,自然地,unpark 方法就是用于唤醒被挂起的线程。
AQS 实现原理概述
有了上述 CLH 和 LockSupprt 的讲述,那么 AQS 如何实现就很明显了。
首先 AQS 中维护了一个 CLH 队列,不过不同于标准的 CLH 队列,AQS 中 CLH 队列,当前置节点中的状态表明的是当前可以获取锁,而不是表示获取锁成功。同样的释放锁也类似。
第二,对于获取锁失败的线程,不是自旋地等待,而是主动挂起,然后当前一个节点释放锁的时候,需要主动的释放下一个线程。
其它
为什么要队列
因为有挂起和唤醒的过程,所以需要能记录被挂起的线程,这是为什么要队列的原因。
为什么是 CLH 队列
其实同步锁除了 CLH 队列之外,还有一个 MCS 队列,之所以要用 CLH 队列主要是相对于 MCS 队列来说,CLH 队列更容易取消和实现超时控制。MCS 队列和 CLH 队列的区别仅仅在于线程是自旋自身节点的 isLocked
,而不是自旋前置节点的 isLocked
。参考 A Hierarchical CLH Queue Lock