前提#
并发编程大师Doug Lea在编写JUC
(java.util.concurrent
)包的时候引入了java.util.concurrent.locks.AbstractQueuedSynchronizer
,其实是Abstract Queued Synchronizer
,也就是"基于队列实现的抽象同步器",一般我们称之为AQS
。其实Doug Lea
大神编写AQS
是有严谨的理论基础的,他的个人博客上有一篇论文《The java.util.concurrent Synchronizer Framewor》,可以在互联网找到相应的译文《JUC同步器框架》,如果想要深入研究AQS
必须要理解一下该论文的内容,然后结合论文内容详细分析一下AQS
的源码实现。本文在阅读AQS
源码的时候选用的JDK
版本是JDK11
。
出于写作习惯,下文会把AbstractQueuedSynchronizer称为AQS、JUC同步器框或者同步器框架。
AQS的主要功能#
AQS
是JUC
包中用于构建锁或者其他同步组件(信号量、事件等)的基础框架类。AQS
从它的实现上看主要提供了下面的功能:
- 同步状态的原子性管理。
- 线程的阻塞和解除阻塞。
- 提供阻塞线程的存储队列。
基于这三大功能,衍生出下面的附加功能:
- 通过中断实现的任务取消,此功能基于线程中断实现。
- 可选的超时设置,也就是调用者可以选择放弃等待任务执行完毕直接返回。
- 定义了
Condition接口
,用于支持管程形式的await/signal/signalAll
操作,代替了Object
类基于JNI
提供的wait/notify/notifyAll
。
AQS
还根据同步状态的不同管理方式区分为两种不同的实现:独占状态的同步器和共享状态的同步器。
同步器框架基本原理#
《The java.util.concurrent Synchronizer Framework》一文中其实有提及到同步器框架的伪代码:
// acquire操作如下:
while (synchronization state does not allow acquire) {
enqueue current thread if not already queued;
possibly block current thread;
}
dequeue current thread if it was queued;
//release操作如下:
update synchronization state;
if (state may permit a blocked thread to acquire){
unblock one or more queued threads;
}
撇脚翻译一下:
// acquire操作如下:
while(同步状态申请获取失败){
if(当前线程未进入等待队列){
当前线程放入等待队列;
}
尝试阻塞当前线程;
}
当前线程移出等待队列
//release操作如下:
更新同步状态
if(同步状态足够允许一个阻塞的线程申请获取){
解除一个或者多个等待队列中的线程的阻塞状态;
}
为了实现上述操作,需要下面三个基本环节的相互协作:
- 同步状态的原子性管理。
- 等待队列的管理。
- 线程的阻塞与解除阻塞。
其实基本原理很简单,但是为了应对复杂的并发场景和并发场景下程序执行的正确性,同步器框架在上面的acquire
操作和release
操作中使用了大量的死循环和CAS
等操作,再加上Doug Lea
喜欢使用单行复杂的条件判断代码,如一个if
条件语句会包含大量操作,AQS
很多时候会让人感觉实现逻辑过于复杂。
同步状态管理#
AQS
内部内部定义了一个32
位整型的state
变量用于保存同步状态:
/**
* The synchronization state.(同步状态值)
*/
private volatile int state;
// 获取state
protected final int getState() {
return state;
}
// 直接覆盖设置state
protected final void setState(int newState) {
state = newState;
}
// CAS设置state
protected final boolean compareAndSetState(int expect, int update) {
return STATE.compareAndSet(this, expect, update);
}
同步状态state
在不同的实现中可以有不同的作用或者表示意义,这里其实不能单纯把它理解为中文意义上的"状态",它可以代表资源数、锁状态等等,下文遇到具体的场景我们再分析它表示的意义。
CLH队列与变体#
CLH
锁即Craig, Landin, and Hagersten (CLH) locks
,因为它底层是基于队列实现,一般也称为CLH
队列锁。CLH
锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。从实现上看,CLH
锁是一种自旋锁,能确保无饥饿性,提供先来先服务的公平性。先看简单的CLH
锁的一个简单实现:
public class CLHLock implements Lock {
AtomicReference<QueueNode> tail = new AtomicReference<>(new QueueNode());
ThreadLocal<QueueNode> pred;
ThreadLocal<QueueNode> current;
public CLHLock() {
current = ThreadLocal.withInitial(QueueNode::new);
pred = ThreadLocal.withInitial(() -> null);
}
@Override
public void lock() {
QueueNode node = current.get();
node.locked = true;
QueueNode pred = tail.getAndSet(node);
this.pred.set(pred);
while (pred.locked) {
}
}
@Override
public void unlock() {
QueueNode node = current.get();
node.locked = false;
current.set(this.pred.get());
}
static class QueueNode {
boolean locked;
}
// 忽略其他接口方法的实现
}
上面是一个简单的CLH
队列锁的实现,内部类QueueNode
只使用了一个简单的布尔值locked
属性记录了每个线程的状态,如果该属性为true
,则相应的线程要么已经获取到锁,要么正在等待锁,如果该属性为false
,则相应的线程已经释放了锁。新来的想要获取锁的线程必须对tail
属性调用getAndSet()
方法,使得自身成为队列的尾部,同时得到一个指向前驱节点的引用pred
,最后线程所在节点在其前驱节点的locked
属性上自旋,直到前驱节点释放锁。上面的实现是无法运行的,因为一旦自旋就会进入死循环导致CPU
飙升,可以尝试使用下文将要提到的LockSupport
进行改造。
CLH
队列锁本质是使用队列(实际上是单向链表)存放等待获取锁的线程,等待的线程总是在其所在节点的前驱节点的状态上自旋,直到前驱节点释放资源。从实际来看,过度自旋带来的CPU性能损耗比较大,并不是理想的线程等待队列的实现。
基于原始的CLH
队列锁中提供的等待队列的基本原理,AQS
实现一种了CLH锁队列的变体(Variant)。AQS
类的protected
修饰的构造函数里面有一大段注释用于说明AQS
实现的等待队列的细节事项,这里列举几点重要的:
AQS
实现的等待队列没有直接使用CLH
锁队列,但是参考了其设计思路,等待节点会保存前驱节点中线程的信息,内部也会维护一个控制线程阻塞的状态值。- 每个节点都设计为一个持有单独的等待线程并且"带有具体的通知方式"的监视器,这里所谓通知方式就是自定义唤醒阻塞线程的方式而已。
- 一个线程是等待队列中的第一个等待节点的持有线程会尝试获取锁,但是并不意味着它一定能够获取锁成功(这里的意思是存在公平和非公平的实现),获取失败就要重新等待。
- 等待队列中的节点通过
prev
属性连接前驱节点,通过next
属性连接后继节点,简单来说,就是双向链表的设计。 CLH
队列本应该需要一个虚拟的头节点,但是在AQS
中没有直接提供虚拟的头节点,而是延迟到第一次竞争出现的时候懒创建虚拟的头节点(其实也会创建尾节点,初始化时头尾节点是同一个节点)。Condition
(条件)等待队列中的阻塞线程使用的是相同的Node
结构,但是提供了另一个链表用来存放,Condition
等待队列的实现比非Condition
等待队列复杂。
线程阻塞与唤醒#
线程的阻塞和唤醒在JDK1.5
之前,一般只能依赖于Object
类提供的wait()
、notify()
和notifyAll()
方法,它们都是JNI
方法,由JVM
提供实现,并且它们必须运行在获取监视器锁的代码块内(synchronized
代码块中),这个局限性先不谈性能上的问题,代码的简洁性和灵活性是比较低的。JDK1.5
引入了LockSupport
类,底层是基于Unsafe
类的park()
和unpark()
方法,提供了线程阻塞和唤醒的功能,它的机制有点像只有一个允许使用资源的信号量java.util.concurrent.Semaphore
,也就是一个线程只能通过park()
方法阻塞一次,只能调用unpark()
方法解除调用阻塞一次,线程就会唤醒(多次调用unpark()
方法也只会唤醒一次),可以想象是内部维护了一个0-1的计数器。
LockSupport
类如果使用得好,可以提供更灵活的编码方式,这里举个简单的使用例子:
public class LockSupportMain implements Runnable {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
private Thread thread;
private void setThread(Thread thread) {
this.thread = thread;
}
public static void main(String[] args) throws Exception {
LockSupportMain main = new LockSupportMain();
Thread thread = new Thread(main, "LockSupportMain");
main.setThread(thread);
thread.start();
Thread.sleep(2000);
main.unpark();
Thread.sleep(2000);
}
@Override
public void run() {
System.out.println(String.format("%s-步入run方法,线程名称:%s", FORMATTER.format(LocalDateTime.now()),
Thread.currentThread().getName()));
LockSupport.park();
System.out.println(String.format("%s-解除阻塞,线程继续执行,线程名称:%s", FORMATTER.format(LocalDateTime.now()),
Thread.currentThread().getName()));
}
private void unpark() {
LockSupport.unpark(thread);
}
}
// 某个时刻的执行结果如下:
2019-02-25 00:39:57.780-步入run方法,线程名称:LockSupportMain
2019-02-25 00:39:59.767-解除阻塞,线程继续执行,线程名称:LockSupportMain
LockSupport
类park()
方法也有带超时的变体版本方法,遇到带超时期限阻塞等待场景下不妨可以使用LockSupport#parkNanos()
。
独占线程的保存#
AbstractOwnableSynchronizer
是AQS
的父类,一个同步器框架有可能在一个时刻被某一个线程独占,AbstractOwnableSynchronizer
就是为所有的同步器实现和锁相关实现提供了基础的保存、获取和设置独占线程的功能,这个类的源码很简单:
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { }
// 当前独占线程的瞬时实例 - 提供Getter和Setter方法
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
它就提供了一个保存独占线程的变量对应的Setter
和Getter
方法,方法都是final
修饰的,子类只能使用不能覆盖。
CLH队列变体的实现#
这里先重点分析一下AQS
中等待队列的节点AQS
的静态内部类Node
的源码:
static final class Node {
// 标记一个节点处于共享模式下的等待
static final Node SHARED = new Node();
// 标记一个节点处于独占模式下的等待
static final Node EXCLUSIVE = null;
// 取消状态
static final int CANCELLED = 1;
// 唤醒状态
static final int SIGNAL = -1;
// 条件等待状态
static final int CONDITION = -2;
// 传播状态
static final int PROPAGATE = -3;
// 等待状态,初始值为0,其他可选值是上面的4个值
volatile int waitStatus;
// 当前节点前驱节点的引用
volatile Node prev;
// 当前节点后继节点的引用
volatile Node next;
// 当前节点持有的线程,可能是阻塞中等待唤醒的线程
volatile Thread thread;
// 下一个等待节点
Node nextWaiter;
// 当前操作的节点是否处于共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
// 获取当前节点的前驱节点,确保前驱节点必须存在,否则抛出NPE
final Node predecessor() {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 空节点,主要是首次创建队列的时候创建的头和尾节点使用
Node() {}
// 设置下一个等待节点,设置持有线程为当前线程
Node(Node nextWaiter) {
this.nextWaiter = nextWaiter;
THREAD.set(this, Thread.currentThread());
}
// 设置waitStatus,设置持有线程为当前线程
Node(int waitStatus) {
WAITSTATUS.set(this, waitStatus);
THREAD.set(this, Thread.currentThread());
}
// CAS更新waitStatus
final boolean compareAndSetWaitStatus(int expect, int update) {
return WAITSTATUS.compareAndSet(this, expect, update);
}
// CAS设置后继节点
final boolean compareAndSetNext(Node expect, Node update) {