一、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
使用了32bit
int
来维护同步状态,因为当时使用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 类继承关系
备注:参考源码为
JDK
1.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 加锁源码:
备注:参考源码为
JDK
1.8 版本。
- 加锁源码入口:
- 尝试失败:
- 阻塞线程:
- 注意:是否需要
unpark()
是由当前节点的前驱节点的waitStatus
==Node.SIGNAL
来决定,而不是本节点的waitStatus
决定。
2.2.3 解锁源码:
备注:参考源码为
JDK
1.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 源码:
- 关键方法及说明: