一、AQS 原理
全称是
AbstractQueuedSynchronizer,它是一个抽象的父类,是阻塞式锁和相关的同步器工具的框架。
1.1 特点
- 用
state属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁:getState()- 获取 state 状态。setState()- 设置 state 状态。compareAndSetState()-cas机制设置 state 状态。- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源。
- 提供了基于
FIFO的等待队列,类似于Monitor的EntryList。 - 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于
Monitor的WaitSet。
1.2 使用
-
子类主要实现这样一些方法(默认抛出
UnsupportedOperationException)tryAcquire()tryRelease()tryAcquireShared()tryReleaseShared()isHeldExclusively()
-
获取锁的姿势:
// 如果获取锁失败
if (!tryAcquire(arg)) {
// 入队, 可以选择阻塞当前线程 park unpark
}
- 释放锁的姿势:
// 如果释放锁成功
if (tryRelease(arg)) {
// 让阻塞线程恢复运行
}
1.3 自定义不可重入锁
- 代码示例:
@Slf4j
public class SyncTests {
/**
* 不可重入测试(与其他线程)。
*/
@Test
public void test01() throws InterruptedException {
MyLock lock = new MyLock();
Thread t1 = new Thread(() -> {
lock.lock();
try {
log.debug("t1 locking...");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
} finally {
log.debug("t1 unlock");
lock.unlock();
}
}, "t1");
Thread t2 = new Thread(() -> {
lock.lock();
try {
log.debug("t2 locking...");
} finally {
log.debug("t2 unlock");
lock.unlock();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// t1 locking...
// t1 unlock
// t2 locking...
// t2 unlock
}
/**
* 不可重入测试(自己也会被挡住)。
* 表现:只会打印一次 locking。
*/
@Test
public void test02() {
MyLock lock = new MyLock();
new Thread(() -> {
lock.lock();
log.debug("[1] locking...");
lock.lock();
log.debug("[2] locking...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
} finally {
log.debug("unlock");
lock.unlock();
}
}).start();
// [1] locking...
}
}
/**
* 一、自定义『不可重入锁』。
*/
class MyLock implements Lock {
/**
* 二、自定义『同步器』。
*/
private static class MySync extends AbstractQueuedSynchronizer {
/**
* 尝试获取锁。
*
* @param arg 参数
* @return boolean
*/
@Override
protected boolean tryAcquire(int arg) {
// 0:表示未上锁;1:表示上锁。
if (compareAndSetState(0, 1)) {
// 设置当前线程为 owner。
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 尝试释放锁,只更改状态,但不会唤醒等待队列的其他线程。
*
* @param arg 参数
* @return boolean
*/
@Override
protected boolean tryRelease(int arg) {
if (getState() == 1) {
setExclusiveOwnerThread(null);
// state 为 volatile 修饰。
setState(0);
return true;
}
return false;
}
/**
* 是否持有独占锁。
*
* @return boolean
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
/**
* 变量条件。
*
* @return {@link Condition}
*/
public Condition newCondition() {
return new ConditionObject();
}
}
/**
* 持有同步器实例。
*/
private final MySync sync = new MySync();
/**
* 尝试,不成功则进入等待队列。
*/
@Override
public void lock() {
sync.acquire(1);
}
/**
* 尝试,不成功则进入等待队列,可打断。
*/
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**
* 尝试一次,不成功返回,不进入队列。
*/
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
/**
* 有时间限制的尝试,不成功进入等待队列。
*/
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
/**
* 释放锁。
*/
@Override
public void unlock() {
sync.release(1);
}
/**
* 生成条件变量。
*/
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
1.4 背景
- 早期程序员会自己通过一种同步器去实现另一种相近的同步器,例如用可重入锁去实现信号量,或反之。这显然不够优雅,于是在
JSR166(java规范提案)中创建了AQS,提供了这种通用的同步器机制。
1.5 功能目标
- 阻塞版本获取锁
acquire()和非阻塞的版本尝试获取锁tryAcquire()。 - 获取锁超时机制。
- 通过打断取消机制。
- 独占机制及共享机制。
- 条件不满足时的等待机制。
1.6 设计思想
AQS的基本思想其实很简单。
- 获取锁的逻辑:
while(// state 状态不允许获取) {
if(// 队列中还没有此线程) {
// 入队并阻塞
}
}
// 当前线程出队
- 释放锁的逻辑:
if(// state 状态允许了) {
// 恢复阻塞的线程(s)
}
-
设计要点一:原子维护 state 状态
state使用volatile配合cas保证其修改时的原子性。state使用了32bitint来维护同步状态,因为当时使用long在很多平台下测试的结果并不理想。
-
设计要点二:阻塞及恢复线程
- 早期的控制线程暂停和恢复的
api有suspend()和resume(),但它们是不可用的,因为如果先调用的resume()那么suspend()将感知不到。 - 解决方法是使用
park()&unpark()来实现线程的暂停和恢复,具体原理在之前讲过了,先unpark()再park()也没问题。 park()&unpark()是针对线程的,而不是针对同步器的,因此控制粒度更为精细。park线程还可以通过interrupt()打断。
- 早期的控制线程暂停和恢复的
-
设计要点三:维护队列
- 使用了
FIFO先入先出队列,并不支持优先级队列。 - 设计时借鉴了
CLH队列,它是一种单向无锁队列。 CLH好处:无锁,使用自旋;快速,无阻塞。
- 使用了
-
示意图:
队列中有
head和tail两个指针节点,都用volatile修饰配合cas使用,每个节点有state维护节点状态入队伪代码,因此只需要考虑tail赋值的原子性。
- 入队伪代码:
do {
// 原来的 tail
Node prev = tail;
// 用 cas 在原来 tail 的基础上改为 node
} while(tail.compareAndSet(prev, node))
- 出队伪代码:
// prev 是上一个节点
while((Node prev=node.prev).state != 唤醒状态) {
}
// 设置头节点
head = node;
二、ReentrantLock 原理
ReentrantLock是可重入的互斥锁(悲观思想),虽然具有与synchronized相同功能,但是会比synchronized更加灵活,ReentrantLock底层基于AbstractQueuedSynchronizer实现。
2.1 类继承关系
备注:参考源码为
JDK1.8 版本。
2.2 非公平锁实现原理
大多数情况下会用到非公平锁,因此也是重点部分。
- 构造器(默认为非公平锁实现):
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
2.2.1 加锁解锁流程:
- 源码入口:
- 开始出现竞争:
-
Thread-1 执行了进入时:
-
CAS尝试将state由 0 改为 1,结果失败; -
进入
tryAcquire()逻辑,这时state已经是1,结果仍然失败; -
接下来进入
addWaiter()逻辑,构造Node队列。
-
-
进入 addWaiter() 逻辑:
-
Node的waitStatus状态,其中 0 为默认正常状态; -
Node的创建是懒惰的; -
其中第一个
Node称为Dummy(哑元)或哨兵,用来占位,并不关联线程。 -
当前线程进入 acquireQueued() 逻辑:
-
acquireQueued()会在一个死循环中不断尝试获得锁,失败后进入park()阻塞; -
如果自己是紧邻着
head(排第二位),那么再次tryAcquire()尝试获取锁,当然这时state仍为 1,失败; -
进入
shouldParkAfterFailedAcquire()逻辑,将前驱node,即head的waitStatus改为 -1,这次返回false; -
shouldParkAfterFailedAcquire()执行完毕回到acquireQueued(),再次tryAcquire()尝试获取锁,当然这时state仍为 1,失败; -
当再次进入
shouldParkAfterFailedAcquire()时,这时因为其前驱node的waitStatus已经是 -1,这次返回true; -
进入
parkAndCheckInterrupt(), Thread-1park()(灰色表示)。 -
多线程竞争失败:
- 尝试释放锁:
-
Thread-0 释放锁,进入
tryRelease()流程,如果成功(设置exclusiveOwnerThread()为null,state = 0); -
当前队列不为
null,并且head的waitStatus= -1,进入unparkSuccessor()流程; -
找到队列中离
head最近的一个Node(没取消的),unpark()恢复其运行,本例中即为 Thread-1; -
回到 Thread-1 的
acquireQueued()流程。 -
发生非公平的竞争:
- 如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了;
- 如果不巧又被 Thread-4 占了先:
- Thread-4 被设置为
exclusiveOwnerThread,state= 1; - Thread-1 再次进入
acquireQueued()流程,获取锁失败,重新进入park()阻塞。
- Thread-4 被设置为
2.2.2 加锁源码:
备注:参考源码为
JDK1.8 版本。
- 加锁源码入口:
- 尝试失败:
- 阻塞线程:
- 注意:是否需要
unpark()是由当前节点的前驱节点的waitStatus==Node.SIGNAL来决定,而不是本节点的waitStatus决定。
2.2.3 解锁源码:
备注:参考源码为
JDK1.8 版本。
- 源码关键部分解读:
2.3 可重入原理
- 源码关键部分解读:
2.4 可打断原理
在此模式下,即使它被打断,仍会驻留在
AQS队列中,一直要等到获得锁后方能得知自己被打断了。
- 『不可打断模式』源码关键部分解读:
- 『可打断模式』源码关键部分解读:
2.5 公平锁实现原理
- 源码关键部分解读:
2.6 条件变量实现原理
每个条件变量其实就对应着一个等待队列,其实现类是
ConditionObject。
2.6.1 await 流程:
- 开始 Thread-0 持有锁,调用 await,进入
ConditionObject的addConditionWaiter()流程; - 创建新的
Node状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部; - 接下来进入
AQS的fullyRelease()流程,释放同步器上的锁。
unpark()AQS队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功;park()阻塞 Thread-0。
2.6.2 signal 流程:
- 假设 Thread-1 要来唤醒 Thread-0:
- 进入
ConditionObject的doSignal()流程,取得等待队列中第一个Node,即 Thread-0 所在Node; - 执行
transferForSignal()流程,将该Node加入AQS队列尾部,将 Thread-0 的waitStatus改为 0,Thread-3 的waitStatus改为 -1; - Thread-1 释放锁,接下来进入
unlock()流程。
2.6.3 源码:
- 关键方法及说明: