theme: juejin
highlight: androidstudio
学习并发,理解并发,掌握并发是Java程序员迈不过去的一道坎。现实业务中很多情况下都会涉及到并发操作,知己知彼百战不殆。学好并发知识非常且极其有必要。这篇文章我们就来分析下并发知识中 非常核心 的一个知识点:AbstractQueuedSynchronizer
简称(AQS)。
在开篇前,我们先看下本文的结构:
说明: 1. 本文jdk版本为:11 1. 为了方便下面我们提到AbstractQueuedSynchronizer类时 一般都用AQS
简称。 2. AQS类本质上是模板方法模式
,所以在学习AQS时,最好要知道什么是模板方法模式知道其结构。 3. 由于AQS内部使用state来表示锁资源,所以本文提到资源时 也大多使用state表示。 4. 在学习AQS之前,希望对锁知识以及Java中的并发工具有所了解和使用,这样可以达到事半功倍的效果。 5. 在AQS中有挺多CAS操作,希望你对CAS有所了解知道他是怎么做的以及使用场景。 6. 由于源码分析比较枯燥,所以本文在每一个源码小节后都会配一张图解(因为一图胜千言,我个人比较喜欢画图来解释一些比较重要或者难懂的知识点
) 7. AQS是JDK层面的锁实现,如果想了解jvm层面的锁实现则移步我的另一篇文章:万字长文分析synchroized:,两者对比下,你会发现似乎有些地方是同样的设计逻辑 8. 本文依赖jdk11源码中有部分和VarHandle相关(比如CLH的入队操作),不了解的最好去看一下VarHandle是个啥。
1、AQS概述
所谓AQS(AbstractQueuedSynchronizer)中文直译抽象队列同步器
,他定义了一套多线程访问共享资源的同步器框架,提供了SDK层面的锁机制,很多类都是基于这个大拿 开发的比如: ReentrantLock/Semaphore/CountDownLatch/ReentrantReadWriteLock/ThreadPoolExecutor中的Worker/以及jdk之外的很多开源项目......等都是基于它。我们简单看下我本地的AQS使用情况:
通过查阅作者的对于该类的文档注释可以得到如下核心信息:
我们来大体概括下上图这段英文信息,就能对AQS有一个基础的认识了。如下:
- AQS用一个
volatile int state
; 属性表示锁状态(因为锁是存在并发获取的,所以必须要被可见的,即保证a修改后b立即在主内存可见!),1表示锁被持有,0表示未被持有AQS类提供了修改该属性的三个方法: getState() , setState(int newState) , compareAndSetState(int expect, int update) 。 - 框架内部维护了一个FIFO的等待队列,是用双向链表实现的,我们称之为CLH队列。
- 框架内部也实现了条件变量 Condition ,用它来实现等待唤醒机制,并且支持多个条件变量(本文我们不做分析留到下篇文章)。
- AQS支持两种资源共享的模式: 独占模式(Exclusive)和共享模式(Share),所谓独占模式就是任 意时刻只允许一个线程访问共享资源,譬如ReentrantLock;而共享模式指的就是允许多个线程同时访 问共享资源,譬如Semaphore/CountDownLatch
- 使用者只需继承 AQS 并重写指定的方法,在方法内完成对共享资源 state 的获取和释放,至于具体线程等待队列的维护,AQS已经在顶层实现好了,在那些模板 方法里。
2、AQS原理与结构
2.1、AQS原理简介
宏观上看,AQS其实就是俩东西组成: 一个是资源 state
,一个是未获取到资源的线程的 等待队列 CLH
。
什么是CLH? : CLH锁其实就是当多个线程竞争同一把锁时,获取不到锁的线程,会排队进入CLH队列(FIFO)的队尾,然后自旋等待,直到其前驱线程释放锁。由于是 Craig、Landin 和 Hagersten三位大佬的发明,因此命名为CLH锁。
CLH队列中存放的是?: 存放的是一个个的Node对象(而Node中存放的东西我们后边再说,总之我们知道Node中一定会有对应的线程信息)
我们先看下AQS大体
的原理图(更多细节的东西在下边会讲到)。
我们简单举个例子,在排他锁模式下流程如下: 1. 假设t1时刻,有线程a持有资源state(持有资源的线程一定是在head节点这个我们一定要清楚
) 2. t1时刻,线程b试图调用获取锁的方法来获取锁资源,发现获取锁失败,则将线程b的相关数据封装为Node并插入CLH队列的队尾。 3. 挂起线程b,并告知线程a(通过将head节点的waitStatus设置为SIGNAL),资源释放了记得通知我啊! 4. t2时刻,线程a释放资源(并将对应Node赋值为null,利于GC)state后通知线程b 5. t3时刻 线程b 尝试获取锁(此时如果是公平锁则大概率可以获取成功,如果是非公平,则不一定)
以上这个只是个大概的流程,期间有很多优化和细节操作。后边源码我们逐一分析在这里我们只需要知道他的主题逻辑就行了。
2.2、AQS结构认识
在分析AQS源码前,我们首先要和AQS内部的兄弟 混个脸熟,所以有了本小节。
首先我们看下 AQS 的 继承关系图,如下: 可以看到 我们常见的并发工具(ReentrantLock/Semaphore/CountDownLatch/ThreadPoolExecutor/ReentrantReadWriteLock),都是直接或间接的继承自AQS类。其实到最后我们会发现,搞懂了AQS这个知识点,上边括号中的那几个并发工具类 原理也就豁然开朗了。
AQS中相当重要的三个成员变量(头/尾节点+state): ```java //头节点(独占锁模式下,持有资源的永远都是头节点!这个要知道哦) private transient volatile Node head; //尾节点 private transient volatile Node tail; //锁资源(无锁状态是0,每次加锁成功后,通过cas进行+1,在重入场景下,重入几次就是几) private volatile int state;
```
AQS中的两个内部类:ConditionObject
和Node
:
Node类:
下边是Node类的源码,先简单看一下: ```java static final class Node { //当前节点处于共享模式的标记 static final Node SHARED = new Node();
//当前节点处于独占模式的标记
static final Node EXCLUSIVE = null;
//线程被取消
static final int CANCELLED = 1;
//head持有锁线程释放资源后需唤醒后继节点
static final int SIGNAL = -1;
//等待condition唤醒
static final int CONDITION = -2;
//工作于共享锁状态,需要向后传播,
static final int PROPAGATE = -3;
//等待状态,有1,0,-1,-2,-3五个值。分别对应上面的值
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后继节点
volatile Node next;
//等待锁的线程
volatile Thread thread;
//等待条件的下一个节点,ConditonObject中用到
Node nextWaiter;
} ```
因为