最近又再研究了一下AQS,颇有所感,做个简单的介绍,望对各位求知若渴的同僚有所帮助
JDK版本:1.8.0_171
AQS是嘛呀?
- AbstractQueuedSynchronizer
- JUC里的同步并发工具,基本上JUC所有的并发工具类(CountDownLautch、Semaphore、CyclicBarrier)都依据了它来进行实现
怎么来真正理解AQS呢?
- 客官别急,我们慢慢来.
-
先自己设计一个简单的独占锁方案实现
实现思路(叫兽勉为其难的牺牲一下吧)
- 有个叫红本本的东西可以来记录下叫兽的♀
- 根据相关政策规定,红本本上只能有一个人噢(独占性质)
- 如果红本本上已经有人了,只有红本本上的那位才能强行占有叫兽,其他的小伙伴只能去排队等待机会了(队列排队)
- 物是人非事事休,欲语泪先流,叫兽的那个ta竟然出柜了!(粉丝团 : 你还有我们呢!),排队的小伙伴里面的头号粉丝同学叫兽是你的人了!(唤醒队列的头号玩家)
我们再初略的看下AQS的整体结构
- 先简略有个印象,然后稍微介绍一下里面的几个点,重点关注我标红的几个字段.
- exclusiveOwnerThread
- 用来记录当前获得独占锁的线程
- head、tail
- 记录我们同步队列的头结点、尾节点
- state
- 状态值标记,不同的同步工具类对它有不同的用法(CountDownLautch,Lock等)
- Node
- 封装的队列节点元素
- prev
- 记录当前节点的上一个节点
- next
- 记录当前节点的下一个节点
- thread
- 记录当前节点的线程
- exclusiveOwnerThread
用AQS的里面的字段来实现一波看看(版本1)
- 嗯,基于上面的设计,貌似实现了简单的独占锁,而且还支持重入(重入指当前线程可以多次获取锁)
- 不过细心的小伙伴有注意到,state字段貌似没见你用呀?
- 实际上ReentrantLock中是通过state字段来达到可重入的效果的.
- state > 0 代表已有线程抢占了锁,加锁了N次,则state = N
来我们再来一版加入state的新版本吧(版本2)
- 可以看到我们根据state来判断当前锁是否被抢占
- 而释放锁的条件变了,只有当state变为0时才会释放锁并通知等待队列中的头结点去尝试抢占锁.
我们将借助Java并发包下的ReentrantLock进行分析
- 可以看到ReentrantLock(可重入锁)内部有一个Sync静态内部类就是继承自我们的AQS
- 同样我们可以看到在底层还有一个FairSync(公平锁)、NonfairSync(非公平锁),这两种类型又是继承自内部的Sync对象,公平和非公平我们先暂且不谈(后续会分析)
/**
* 可以看到默认构造是非公平的实现
*/
public ReentrantLock() {
sync = new NonfairSync();
}
下面将真正开始秋名漂移时刻!
- 关键方法
- lock
- unlock
public void lock() {
// 哦豁,这里的lock实际上就是调用的我们初始化的时候NonfairSync了
sync.lock();
}
接着往下瞅瞅看
/**
* 非公平锁实现
*/
static final class NonfairSync extends Sync {
final void lock() {
// 这里的CAS操作相当于两个操作组合
// 1. 当前的state = 0 ?
// 2.等于0的情况将state设置为1
if (compareAndSetState(0, 1))
//登记当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
//尝试失败了,说明之前state的值不是0,有人先抢到锁了
acquire(1);
}
}
细心的小伙伴发现了,这个实现思路不就是和我们上面的设计如出一辙?
那么我们可以大胆的猜测,是不是else里的acquire(1)将会出现我们设计里的排队操作呢?
咋们拭目以待:
public final void acquire(int arg) {
// tryAcquire顾名思义,就是再试试能抢得到锁不
// addWaiter其实就是往队列里加入新的节点
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
接下来摆在我们面前的有三个点
- tryAcquire
- addWaiter
- acquireQueued
先来看下tryAcquire
//回到了我们的NonfailSync类中
protected final boolean tryAcquire(int acquires) {
//下面这个操作实际是定义在Sync抽象类中的
return nonfairTryAcquire(acquires);
}
----------------------华丽的分割线(下面部分是Sync类中)-----------------------
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//c==0意味着锁被空出来了
if (c == 0) {
//又尝试去抢锁(下面的操作是否似曾相识?)
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//state!=0,判断当前线程是不是就是抢到锁的那个
else if (current == getExclusiveOwnerThread()) {
//哦豁,是自己本身就把state值增加
int nextc = c + acquires;
//这里是考虑到int类型溢出了,变成了负数
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//尝试抢锁失败
return false;
}
我们看上面这部分代码是否感到似曾相识的感觉呢?
可以看到tryAcquire其实就是一次尝试抢锁的过程.
接下来再看看addWaiter是干嘛呢
private Node addWaiter(Node mode) {
//新创建了个节点,mode其实就是标明节点的模式,我们当前是Node.EXCLUSIV模式
Node node = new Node(Thread.currentThread(), mode);
//下面这里其实是快速的尝试一次能不能直接插到队列的尾部去
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//尾部节点为空(说明队列还没初始化呢),以及CAS设置尾部失败了(存在并发竞争)会走enq的入队操作
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 尾部节点为空时会初始化一个头节点,这个头节点是一个dummy节点
if (compareAndSetHead(new Node()))
tail = head;
} else {//自旋循环第二次的时候,因为第一次设置了头尾节点,会走到else逻辑
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
可以看到addWaiter其实就是将我们的线程封装成了Node节点,并且加入到了链表队列的尾部.
接下来看看 acquireQueued,这个就要画重点了!
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
//中断标志
boolean interrupted = false;
//自旋尝试获取锁
for (;;) {
//取得当前节点的上一个节点
final Node p = node.predecessor();
//上一个节点是头节点并且尝试获取锁成功;tryAcquire上面有分析过是尝试获取锁的过程
if (p == head && tryAcquire(arg)) {
//抢到锁了会把头节点替换成自己本身
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 没抢到锁的线程在自旋里会在这一步阻塞挂起
// 如果队列里的头个等待节点后面被唤醒了,它又会再次自旋去尝试抢锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//异常情况下,会将node节点置位取消状态
if (failed)
cancelAcquire(node);
}
}
初步看下来,可以看到acquireQueued是队列中的等待节点自旋尝试获取锁的一个过程,中间会存在线程挂起阻塞
shouldParkAfterFailedAcquire 这个方法的逻辑需要一点前置知识的铺垫.
-
我们再来回忆下之前的结构图
- 上面可以看到Node里有一个 waitStatus的状态字段,这个字段非常重要,我们马上就可以看到它的出场
-
waitStatus 一共有五种值,默认为0,其他四种值如下
1. CANCELLED = 1; (取消状态)
2. SIGNAL = -1; (这个状态表示该节点的下个节点是需要unpark的)
3. CONDITION = -2; (这个状态用在等待队列中)
4. PROPAGATE = -3; (这个状态意味着节点是带传播属性的) - CONDITION、PROPAGATE两种状态分别是等待队列CondtionObject中、共享模式的锁实现中会使用到,本次暂不做过多分析.
接下来完成我们上面未完成的工作,shouldParkAfterFailedAcquire
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取当前的节点Node的上一节点pred的状态值
int ws = pred.waitStatus;
//哦豁,如果上一节点status已经是SIGNAL
//即表示当前节点是可以支持做unpark操作的,所有可以放心挂起线程了,返回true
if (ws == Node.SIGNAL)
return true;
// 什么情况下ws会大于0?
// 回忆了一下,CANCELLED,只有这个状态值为1是大于0的,说明节点被取消了
if (ws > 0) {
//这个循环操作其实就是过滤掉被取消的节点,直到找到一个正常节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {//如果是正常节点的话,就把上一节点打上SIGNAL标记
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
有了上面的状态值分析后,再来看该方法,是不是清晰了许多
可以看到shouldParkAfterFailedAcquire其实就是检查当前即将要挂起的节点的上一节点,确保上一节点的状态值是SIGNAL状态
此時再回到之前的這個if判斷
//shouldParkAfterFailedAcquire 确保SIGNAL状态
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
private final boolean parkAndCheckInterrupt() {
//只是一个工具类,将当前线程进行挂起
LockSupport.park(this);
//检查了下线程是否发生过中断,并中断复位
return Thread.interrupted();
}
可以看到之前的这一个if条件其实就是首先确保当前线程的节点上一节点的状态值为SIGNAL,然后将线程进行挂起操作.
看到这里,我们的一个加锁操作的分析就已经结束了,为大伙儿鼓个掌!
下回会进行锁释放的一个分析,有兴趣的小伙伴可以提前自己了解下锁释放的实现!
我们最后再来总结一下,先上个图
- state用来表明有锁、无锁状态
- state = 0 ,无锁状态
- state > 0, 表明加锁次数
- exclusiveOwnerThread来记录获取到锁的线程,主要是为了支持可重入.
- 未获取到锁的线程封装成Node节点组成双向链表排队等待
- Node节点有一个waitStatus状态值,默认为0,线程Node节点挂起前会将其上一个节点状态设置为SIGNAL
彩蛋
之前有提到过公平锁、非公平锁,默认的可重入锁是非公平的,那么不公平体现在哪呢?
- 非公平锁
- 每次lock会自己先尝试下能否抢到锁,抢不到才会加入同步队列挂起
- 这对已经在队列等着抢锁的小伙伴无疑是不公平的,总有人想先插个队试试
- 公平锁
- 即便现在是无锁状态,这位公平的小青年也会看下同步队列里是否已经有人在等着,有的话自己乖乖去排队.
有兴趣的小伙伴可以自己研读下公平锁的实现方式!
- 即便现在是无锁状态,这位公平的小青年也会看下同步队列里是否已经有人在等着,有的话自己乖乖去排队.
尾声
好了!就先到这吧!
后续会持续更新相关的锁释放环节、Condition条件等待队列、CountDownLautch、Semaphore、CyclicBarrier、Future、ArrayBlockingQueue、线程池的原理等,有兴趣的同学可以敬请期待!
作者: 沃尔特叫兽