AQS 全称是 Abstract Queued Synchronizer,一般翻译为同步器。是一套实现多线程同步功能的框架,由 Doug Lea 操刀设计并开发实现的。AQS 在源码中被广泛使用,尤其是在 JUC(Java Util Concurrent)中,比如 ReentrantLock,Semaphore,CountDownLatch,ThreadPoolExecutor。
我们通过 ReentrantLock 与 AQS 的关系来理解 AQS 的内部的工作机制,首先从 ReentrantLock 的 lock() 方法开始。代码很简单,只是调用了 sync 的 lock() 方法。
这个 Sync 是什么呢?它是 ReentrantLock 里的一个静态内部类。ReentrantLock 并没有直接继承 AQS,而是通过内部类 Sync 来扩展 AQS 的功能。然后,ReentrantLock 中存有 Sync 的全局引用。
Sync 在 ReentrantLock 中有两种实现:NonfairSync 和 fairSync,分别对应非公平锁和公平锁。非公平锁实现源码如下:
在非公平锁的 lock() 方法中,如果通过 CAS 设置变量 state 成功,表示当前线程获取锁成功,则将当前线程设置为独占线程。如果通过 CAS 设置变量 state 失败,表示当前锁正在被其它线程持有,则进入 acquire() 方法进行后续处理。acquire() 方法定义在 AQS 中,具体如下
acquire() 是一个比较重要的方法,可以将其拆解为 3 个主要步骤:
1. tryAcquire() 方法主要目的是尝试获取锁;
2. addWaiter() 如果 tryAcquire() 尝试获取锁失败则调用 addWaiter() 将当前线程添加到一个等待队列中;
3. acquireQueued() 处理加入到队列中的节点,通过自旋去尝试获取锁,根据情况将线程挂起或者取消。
以上3种方法都被定义在 AQS 中,但 tryAcquire() 有点特殊,其实现如下
默认情况下直接抛出异常,因此它需要在子类中复写。也就是说真正获取锁的逻辑由子类同步器自己实现。
RenntrantLock 中 tryAcquire() 方法以非公平锁为例如下
获取当前线程,判断当前锁的状态,如果 state = 0,表示当前是无锁的状态。通过 CAS 更新 state 的值,返回 true。如果是同一个线程重入,则直接增加重入次数。上述情况都不满足,则获取锁失败,返回 false。
下面用一张图表示 ReentrantLock 的 lock() 方法的过程
从图中可以看出,在 ReentratLock 执行 lock() 的方法中,大部分同步机制的核心逻辑都已经在 AQS 中实现。ReentrantLock 只要自身实现某些特定步骤下的方法即可。这种设计模式叫做模板模式。在 Android 中,这种模式很常见。比如 Activity 的声明周期都已经在 framwork 中定义好了。子类 Activity 只要在相应的 onCreate()、onPause() 等声明周期方法中提供相应的实现即可。
JUC 包中其他组件,例如 CountDownLatch、Semaphor 等。都是通过一个内部类 Sync 来继承 AQS,然后在内部中通过操作 Sync 来实现同步。好处是将线程的控制逻辑控制在 Sync 内部,而对外面向用户提供的接口是自定义锁。
AQS 中几个关键属性,Node 和 state,如下图所示:
state 表示当前锁状态
state = 0 时表示无锁状态;
state > 0 时,表示已经有线程获得了锁,也就是 state = 1。如果同一个线程多次获得同步锁的时候,state 会递增,比如重入 5 次,那么 state = 5。而在释放锁的时候,同样需要释放 5 次直到 state = 0,其他线程才有资格获得锁。
state的一个功能是实现锁的独占模式或者共享模式
独占模式: 只有一个线程能够持有同步锁。比如在独占模式下,可以把state的初始值设置成0,当某个线程申请锁对象时,需要判断state的值是不是0,如果不是0的话意味着其他线程已经持有该锁,则本线程需要阻塞等待。
共享模式: 可以有多个线程持有同步锁。比如某项操作允许10个线程同时进行,超过这个数量的线程就需要阻塞等待,那么只需要在线程申请对象时判断state的值是否小于10。如果<10,就将 state 加 1 后继续同步语句的执行;如果等于 10,说明已经有 10个线程在同时执行该操作,本线程需要阻塞等待。
Node 双端队列节点
Node 是一个先进先出的双端队列,并且是等待队列。当多线程争用资源被阻塞时会进入此队列。这个队列是 AQS 实现多线程同步的核心,在 AQS 中有两个 Node 指针,分别指向队列的 head 和 tail。Node 的主要结构如下
默认情况下,AQS 中的链表结构如下图所示
获取锁失败后续流程分析
锁的意义是使竞争到锁对象的线程执行同步代码块。多个线程竞争锁时,竞争失败的线程需要被阻塞等待后续唤醒。那么,ReentrantLock 是如何实现让线程等待并唤醒的呢?
在 ReentrantLock.lock() 阶段,在 acquire() 方法中会先后调用 tryAcquire、addWaiter、acquireQueued 来处理。tryAcquire 在 ReentrantLock 中被复写并实现。如果返回 true 说明成功获取锁,就继续执行同步代码语句。
如果 tryAcquire 返回 false,那么当前线程会被 AQS 如何处理呢?
1. 通过 addWaiter()方法把线程加入到 node 等待队列中。
首先,当前获取锁失败的线程会被添加到一个等待队列的末端,具体源码如下
有两种情况会使插入队列失败:1)tail 为空,说明队列从未初始化。因此需要调用 enq 方法在队列中插入一个空的 Node;2)CompareAndSetTail 失败,说明插入过程中有线程修改了此队列。因此,需要调用 enq() 将当前 Node 重新插入到队列末端。经过 addWaiter() 方法之后,此时线程以 Node 的方式被加入到队列的末端。但是线程还没有执行阻塞操作,真正的阻塞操作是在 acquireQueued() 方法中。
2. 在 acquireQueued 方法中把线程阻塞
在 acquireQueued 方法中并不会立即挂起该节点中的线程,在插入节点的过程中,之前持有锁的线程可能已经执行完毕并释放锁,这里使用自旋再次去尝试获取锁。如果自旋后还是没有获取到锁,则将该线程挂起或阻塞。acquireQueued() 的源码如下
可以看出在,shouldParkAfterFailedAcquire() 方法中会判断该线程是否应该被阻塞。其代码如下
首先,获取前驱节点的 waitStates 值。Node 中的 waitStates 值一共有5种取值,分别代表的意义如下。
接下来,根据 waitStates 的值进行不同的操作,主要有以下几种情况
如果 waitStatus 等于 SIGNAL,返回 true 将当前线程挂起,等待后续唤醒操作即可;
如果 waitStatus 大于 0 也就是 CANCLE 状态,会将此前驱节点从队列中删除,并在循环中逐步寻找下一个不是 “CANCEL”状态的节点作为当前节点的前驱节点。
如果 waitStatus 既不是 SINGAL 也不是 CANCEL,则将当前节点的前驱节点状态设置为 SIGNAL。好处是下一次执行 shouldParkAfterFailedAcquire 时可以直接返回 true,挂起线程。
如果 shouldParkAfterFailedAcquire 返回 true 表示线程需要被挂起,那么会继续调用 parkAndCheckInterrupt 方法执行真正的阻塞线程代码。
这个方式只调用了 LockSupport 中的 park() 方法。在LockSupport.park() 方法中调用了 Unsafe API 来执行底层 Native 方法,将线程挂起。
获取锁的大体流程如下:
1. AQS 的模板方法 acquire 通过调用子类自定义实现的 tryAcquire 获取锁;
2. 如果获取锁失败,通过 addWaiter 方法将线程构造成 Node 节点插入到同步队列队尾;
3. 在 acquireQueued 方法中以自旋的方式尝试获取锁。如果失败则判断是否需要将当前线程阻塞;如果需要阻塞则最终执行 LockSupport(Unsafe)中的 native API 来实现线程阻塞。
AQS 是如何尝试唤醒等待队列中被阻塞的线程呢?
上面加锁阶段,被阻塞的线程需要重新唤醒才能继续执行。
同加锁过程一样,释放锁需要从 ReentrantLock 的 unlock() 开始。具体代码如下
可以看出,首先调用 tryRelease 方法尝试释放锁,如果成功,最终会调用 AQS 的 unparkSuccessor() 来实现释放锁的操作。unparkSuccessor 具体实现如下
不管在加锁还是释放锁的阶段,都会用到一种通用的操作:compareAndSetXXX,这种操作最终会调用 Unsafe 中的 API 进行 CAS 操作。
CAS 全称是 Compare And Swap,译为比较和替换,是一种通过硬件实现并发安全的常用技术。底层通过利用 CPU 的 CAS 指令对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。实现过程主要有3个操作数:1)内存值V;2)旧的预期值E;3)要修改的新值U。
当且仅当预期值 E 和内存值 V 相同时,才将内存值 V 修改为 U,否则什么都不做。CAS 底层会根据操作系统和处理器的不同来选择对应的调用代码。以 Windows 和 X86 处理器为例
如果是多处理器。通过带 lock 前缀的 cmpxchg 指令对缓存加锁或总线程加锁的方式来实现多处理器之间的原子操作。
如果是单处理器。通过 cmpxchg 指令完成原子操作。
总结
AQS是一套框架,在框架内部已经封装好了大部分同步需要的逻辑。在AQS内部维护了一个状态指示器state和一个等待队列Node。AQS有两种不同的实现:
● 独占锁(ReentrantLock等)
● 分享锁(CountDownLatch、 读写锁等)
本次主要从独占锁的角度深入分析了AQS的加锁和释放锁的流程。
理解 AQS 的原理对理解 JUC 包中其他组件实现的基础有帮助。可能需要子类同步器实现的方法如下:
lock()
tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false;
tryRelease(int):读占方式。尝试释放资源,成功 则返回 true,失败则返回 false;
tryAcquireShared(int):共享方式。尝试获取资源,负数表示失败;0表示成功,但没有剩余可用资源;整数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待节点返回 true,否则返回 faltata