前言
Java中大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为 AQS)实现的。
AQS 是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。
本文会先介绍应用层,再逐渐深入介绍原理层。通过介绍ReentrantLock 的基本特性和 ReentrantLock 与 AQS 的关联,来深入解读 AQS 相关独占锁的知识点。
本篇文章主要阐述 AQS 中独占锁的逻辑和 Sync Queue, 不包含共享锁和 Condition Queue 的部分(本篇文章核心为 AQS 原理剖析,只是简单介绍了 ReentrantLock,感兴趣同学可以阅读一下 ReentrantLock 的源码)。
下面列出本篇文章的大纲和思路,以便于大家更好地理解:
ReentrantLock
ReentrantLock 与synchronized的区别和使用
ReentrantLock 意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。为了帮助大家更好地理解 ReentrantLock 的特性,我们先将 ReentrantLock 跟常用的 Synchronized 进行比较,其特性如下(蓝色部分为本篇文章主要剖析的点):
ReentrantLock | Synchronized | |
---|---|---|
锁实现机制 | 依赖AQS | 监视器模式(monitor) |
灵活性 | 支持响应中断、超时、尝试获取锁 | 不灵活 |
锁释放方式 | 必须显式调用unloc() | 自动释放 |
锁类型 | 公平锁&非公平锁 | 非公平锁 |
条件队列 | 可关联多个条件队列 | 关联一个 |
可重入性 | 可重入 | 可重入 |
ReentrantLock:
public void test () throw Exception {
// 1. 初始化选择公平锁、非公平锁
ReentrantLock lock = new ReentrantLock(true);
// 2. 可用于代码块
lock.lock();
try {
try {
// 3. 支持多种加锁方式,比较灵活 ; 具有可重入特性
if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
} finally {
// 4. 手动释放锁
lock.unlock()
}
} finally {
lock.unlock();
}
}
Synchronized:
// **************************Synchronized 的使用方式
**************************
// 1. 用于代码块
synchronized (this) {}
// 2. 用于对象
synchronized (object) {}
// 3. 用于方法
public synchronized void test () {}
// 4. 可重入
for (int i = 0; i < 100; i++) {
synchronized (this) {}
}
// **************************ReentrantLock 的使用方式
**************************
ReentrantLock 与 AQS 的关联
我们知道ReentrantLock 支持公平锁和非公平锁,并且 ReentrantLock的底层就是由 AQS 来实现的。那么 ReentrantLock 是如何利用AQS实现公平锁和非公平锁呢? 我们着重从这两者的加锁过程来理解一下它们与 AQS 之间的关系(加锁过程中与 AQS 的关联比较明显,解锁流程后续会介绍)
非公平锁源码中的加锁流程如下:
// java.util.concurrent.locks.ReentrantLock#NonfairSync
// 非公平锁
static final class NonfairSync extends Sync {
...
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
...
}
上述代码解释如下:
- 若通过 CAS 设置变量 State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。
- 若通过 CAS 设置变量 State(同步状态)失败,也就是获取锁失败,则进入Acquire 方法进行后续处理。
但第二步获取锁失败后,后续的处理策略是怎么样的呢?这块可能会有以问题:
- 某个线程获取锁失败的后续流程是什么呢?有以下两种可能:
- 将当前线程获锁结果设置为失败,获取锁流程结束。
这种设计会极大降低系统的并发度,并不满足我们实际的需求。所以就需要下面这种流程,也就是AQS 框架的处理流程。
-
存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。
-
对于问题 1 的第二种情况,既然说到了排队等候机制,那么就一定会有某种队列结构。
- 这种队列是什么数据结构呢?
- 处于排队等候机制中的线程,什么时候可以有机会获取锁呢?
- 如果处于排队等候机制中的线程一直无法获取锁,还是需要一直等待吗,还是有别的策略来解决这一问题?
带着非公平锁的这些问题,再看下公平锁源码中获锁的方式:
// java.util.concurrent.locks.ReentrantLock#FairSync
static final class FairSync extends Sync {
...
final void lock() {
acquire(1);
}
...
}
对这块代码,我们可能会存在这种疑问:Lock 函数通过 Acquire 方法进行加锁,但是具体是如何加锁的呢?
结合公平锁和非公平锁的加锁流程,虽然流程上有一定的不同,但是都调用了Acquire 方法,而 Acquire 方法是 FairSync 和 UnfairSync 的父类 AQS 中的核心方法。
对于上边提到的问题,其实在 ReentrantLock 类源码中都无法解答,而这些问题的答案都在 Acquire 方法所在的类 AbstractQueuedSynchronizer 中,也就是本文的核心——AQS。下面我们会对 AQS 以及ReentrantLock 和 AQS 的关联做详细介绍。
AQS
首先,简单描述下 AQS 框架总的来说,AQS 框架共分为五层:
API层, 锁获取方法层,队列方法层,排队方法层,数据提供层
当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。
当自定义同步器进行加锁或者解锁操作时,先经过第一层的 API 进入 AQS 内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。
下面我们会从整体到细节,从流程到方法逐一剖析 AQS 框架,主要分析过程
如下:
原理概览
AQS 核心思想是:
- 如果被请求的共享资源是空闲状态,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;
- 如果共享资源被占用了,就需要一定的阻塞等待唤醒机制来保证锁分配。
这个机制主要用的是 CLH 队列的变体实现的,将暂时获取不到锁的线程加入到队列中。
CLH:Craig、Landin and Hagersten 队列,是单向链表,AQS 中的队列是CLH 变体的虚拟双向队列(FIFO)。
AQS 是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
主要原理图如下:
AQS 使用一个** Volatile 的 int 类型的成员变量来表示同步状态**,通过内置的FIFO 队列来完成资源获取的排队工作,通过 CAS 完成对 State 值的修改。
AQS 数据结构
这里先看下 AQS 中最基本的数据结构——Node,Node 即为上面 CLH 变体队列中的节点。
方法和属性值 | 含义 |
---|---|
waitStatus | 当前节点在队列中的状态 |
thread | 表示处于该节点的线程 |
prev | 前驱指针 |
predecessor | 返回前驱节点,没有的话抛出 npe |
nextWaiter | 指向下一个处于 CONDITION 状态的节点(由于本文不讲述 Condition Queue 队列,这个里不多介绍) |
next | 后继指针 |
线程获取/等待的锁的两种模式:
模式 | 含义 |
---|---|
SHARED | 共享锁:表示线程以共享的模式等待锁 |
EXCLUSIVE | 独占锁:表示线程正在以独占的方式等待锁 |
waitStatus 有下面几个枚举值:
枚举 | 含义 |
---|---|
0 | 当一个 Node 被初始化的时候的默认值 |
CANCELLED | 为 1,表示线程获取锁的请求已经取消了 |
CONDITION | 为 -2,表示节点在等待队列中,节点线程等待唤醒 |
PROPAGATE | 为 -3,当前线程处在 SHARED 情况下,该字段才会使用 |
SIGNAL | 为 -1,表示线程已经准备好了,就等资源释放了 |
同步状态 State
在了解数据结构后,接下来了解一下 AQS 的同步状态——State。
AQS 中维护了一个** state 的字段,意为同步状态,是由 Volatile 修饰的,用于展示当前临界资源的获锁情况**。
// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;
下面提供了几个访问这个字段的方法:
方法名 | 描述 |
---|---|
protected final int getState() | 获取 State 的值 |
protected final void setState(int newState) | 设置 State 的值 |
protected final boolean compareAndSetState(int expect, int update) | 使用 CAS 方式更新 State |
这几个方法都是 Final 修饰的,说明子类中无法重写它们。
我们可以通过修改 State 字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程):
独占模式:
共享模式:
如果我们要自定义的同步工具,需要自定义获取同步状态和释放状态的方式,也就 是 AQS 架构图中的第一层:API 层。