【洞悉AQS】通过ReentrantLock一步一图彻底了解AQS实现原理
前言
谈到并发,我们不得不说AQS(AbstractQueuedSynchronizer)
,所谓的AQS
即是抽象的队列式的同步器,内部定义了很多锁相关的方法,例如:
- getState():获取锁的标志state值
- setState():设置锁的标志state值
- tryAcquire(int):独占方式获取锁。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式释放锁。尝试释放资源,成功则返回true,失败则返回false。
这里还有更多的方法并没有列出来,我们以ReentrantLock
作为突破点通过源码和画图的形式一步步了解AQS
内部实现原理。
目录结构
文章准备模拟场景来进行解析:
三个线程(线程一、线程二、线程三)同时来加锁/释放锁,然后通过代码和画图一步步解析其中的实现。
目录如下:
- 线程一加锁成功时
AQS
内部实现 - 线程二/三加锁失败时
AQS
中等待队列的数据模型 - 线程一释放锁及线程二获取锁实现原理
这里会分析每个线程加锁、释放锁内部的一系列实现原理
AQS实现原理
AQS
中 维护了一个volatile int state
(代表共享资源)和一个FIFO
线程等待队列(多线程争用资源被阻塞时会进入此队列)。
这里volatile
能够保证多线程下的可见性,当state=1
则代表当前对象锁已经被占有,其他线程来加锁时则会失败,然后线程放入一个FIFO
的等待队列中使用UNSAFE.park()
来挂起当前线程。
另外state
的操作都是使用CAS
来保证其并发修改的安全性。
具体原理我们可以用一张图来简单概括:(此图片来源:https://www.cnblogs.com/waterystone/p/4920797.html)
image.png
场景分析
线程一加锁成功
如果同时有三个线程并发抢占锁,此时线程一抢占锁成功:
此时线程二、线程三加锁失败:
抢占锁代码实现:
java.util.concurrent.locks.ReentrantLock .NonfairSync:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
这里使用的ReentrantLock非公平锁,线程进来直接利用CAS
尝试抢占锁,如果抢占成功则state
被改为1,然后设置独占锁线程为当前线程。如下所示:
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
线程二抢占锁失败
我们按照真实场景来分析,此时线程一抢占锁成功,state
变为1,所以线程二通过CAS
修改state
变量必然会失败。此时AQS
中FIFO
队列中数据如图所示:
我们将线程二执行的逻辑一步步拆解来看:
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire()
:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
先看看tryAcquire()
的具体实现:java.util.concurrent.locks.ReentrantLock .nonfairTryAcquire()
:
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state状态,线程一加锁成功了,此时state=1
int c = getState();
// 如果state=0,说明可以尝试利用CAS进行加锁操作
if (c == 0) {
// 加锁成功的逻辑
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程和独占锁线程是同一个,那么可以重入加锁
// 重入加锁后,state=2
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 返回false加锁失败
return false;
}
这段代码走下来,此时全局变量state
=1,所以通过CAS
修改state
的值不会成功。
而此时持有锁的线程是线程一,所以线程二*也不满足重入的条件。
线程二执行tryAcquire()
后返回false,接着执行addWaiter(Node.EXCLUSIVE)
,代码实现如下:
java.util.concurrent.locks.AbstractQueuedSynchronizer.addWaiter()
:
private Node addWaiter(Node mode) {
// mode = Node EXCLUSIVE = null
// 创建一个新的node,thread = 线程二,nextWaiter = null
Node node = new Node(Thread.currentThread(), mode);
// 此时tail = null,直接执行enq操作
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 线程二直接进入队列操作
enq(node);
return node;
}
此时tail
指针为空,直接调用enq(node)
将当前线程加入等待队列尾部:
private Node enq(final Node node) {
for (;;) {
// 第一次循环tail = null
Node t = tail;
if (t == null) {
// 用CAS将head设置为一个新Node
if (compareAndSetHead(new Node()))
// tail 和 head都指向这个新Node
tail = head;
} else {
// 第二次循环进入,node的前置节点设置为tail = head
node.prev = t;
// 用CAS操作设置tail节点为当前传入的node节点
if (compareAndSetTail(t, node)) {
// t开始时指向tail=head节点,通过CAS将tail指向node后
// 设置t.next=head.next=node
t.next = node;
// 返回t=head节点
return t;
}
}
}
}
第一遍循环tail
指针为空,进入if逻辑中,此时队列中数据:
执行完成之后,head
、tail
、t
都指向第一个元素(new Node()
)
接着执行第二遍循环,进入else逻辑,此时已经有了head节点,这里要操作的就是将线程二这个Node节点挂到head
节点上来。
addWaiter()
方法执行完后会返回当前插入线程二构建的Node节点,此时队列中的数据为:
再接着看看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
执行逻辑,此时传入的为线程二构建的Node
信息:
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued()
:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// p为线程二Node的前置节点,也就是head节点
final Node p = node.predecessor();
// p= head成立,继续使用CAS尝试加锁,此时线程一还在持有锁
// state = 1,所以加锁失败
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 执行shouldParkAfterFailedAcquire方法
// shouldParkAfterFailedAcquire方法中将head节点的waitStatus变为了SIGNAL=-1
// 接着执行parkAndChecknIterrupt,调用LockSupport.park()
// 挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndChecknIterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// ws为head节点的waitStatus=null
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 通过CAS操作,将head节点的waitStatus变为SIGNAL=-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 调用底层的park方法,挂起当前线程
LockSupport.park(this);
return Thread.interrupted();
}
acquireQueued()
这个方法会先判断当前传入的Node
对应的前置节点是否为head
,如果是则尝试加锁。加锁成功过则将当前节点设置为head
节点,然后空置之前的head
节点。
如果加锁失败或者Node
的前置节点不是head
节点,首先将Node
的前置节点中的waitStatus
设置为SIGNAL
(值为-1),
然后挂起当前Node
节点(当前Node
为线程二创建的节点),操作后AQS队列中的数据如下图:
此时线程二就静静的待在AQS
的等待队列里面了,等着其他线程释放锁来唤醒挂起的线程。
线程三抢占锁失败
看完了线程二抢占锁失败的分析,那么再来分析线程三抢占锁失败就很简单了,先看看addWaiter(Node mode)
方法:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// pred = tail = 线程二
Node pred = tail;
// 此时pred不为空
if (pred != null) {
// 设置线程三的前置节点为线程二
node.prev = pred;
// 使用CAS将线程三设置为tail节点
if (compareAndSetTail(pred, node)) {
// pred = 线程二的next节点设置为线程三
pred.next = node;
// 返回线程三节点
return node;
}
}
enq(node);
return node;
}
执行完后AQS
中队列数据如图:
接着执行acquireQueued()
方法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// p = 线程三的前置节点= 线程二
final Node p = node.predecessor();
// 判断线程二是否是head节点,如果是则尝试抢占锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 执行shouldParkAfterFailedAcquire方法
// 将线程三的前置节点线程二中的waitStatus变为SIGNAL = -1
// 然后执行parkAndCheckInterrupt将线程三也挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
执行完后AQS
中队列数据如图:
线程一释放锁
现在来分析下释放锁的过程,首先是线程一释放锁,释放锁后会唤醒head
节点的后置节点,也就是我们现在的线程二,执行完后AQS
队列数据如下:
此时线程二已经被唤醒,继续尝试获取锁,如果获取锁失败,则会继续被挂起。如果获取锁成功,则AQS
中数据如图:
接着还是一步步拆解来看,先看看线程一释放锁的代码:
java.util.concurrent.locks.AbstractQueuedSynchronizer.release()
public final boolean release(int arg) {
// tryRelease() 方法实现在ReentrantLock中实现的
if (tryRelease(arg)) {
// 如果释放锁成功,定义h=head节点
Node h = head;
// 如果head不为空,此时head.waitStatus=SIGNAL=-1
if (h != null && h.waitStatus != 0)
// 执行唤醒操作,唤醒之前挂起的线程
unparkSuccessor(h);
return true;
}
return false;
}
此时看ReentrantLock.tryRelease()
中的具体实现:
protected final boolean tryRelease(int releases) {
// getState()是获取state变量,此时state=1
// releases传入进来的也是1,所以c=0
int c = getState() - releases;
// 如果释放锁的线程,不是当前独占锁线程,直接抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果c=0则说明释放锁成功
if (c == 0) {
free = true;
// 设置独占锁线程为null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
执行完ReentrantLock.tryRelease()
后,state
被设置成0,Lock对象的独占锁被设置为null。可以看下执行后AQS中的数据:
接着执行java.util.concurrent.locks.AbstractQueuedSynchronizer.unparkSuccessor()
方法,唤醒head
的后置节点:
private void unparkSuccessor(Node node) {
// node为head线程,此时node.waitStatus=SIGNAL=-1
int ws = node.waitStatus;
if (ws < 0)
// 设置node节点的waitStatus为0
compareAndSetWaitStatus(node, ws, 0);
// s=node.next=head.next,获取线程二Node节点
Node s = node.next;
// 此时s.waitStatus=SIGNAL=-1 条件不成立
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 执行LockSupport.unpark,这里是唤醒线程二
if (s != null)
LockSupport.unpark(s.thread);
}
逻辑如图:
此时线程二被唤醒,线程二接着之前被park
的地方继续执行,继续执行acquireQueued()
方法。
线程二唤醒继续加锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取线程二的前置节点为head
final Node p = node.predecessor();
// p=head,然后尝试加锁,对state进行CAS操作
// 此时仍有可能加锁失败,因为我们使用的ReentrantLock中的非公平锁
// 如果真好有新的线程进来挣钱锁,线程二就有可能加锁失败
// 如果加锁失败就还会被挂起
if (p == head && tryAcquire(arg)) {
// 线程二加锁成功,将线程二设置为head节点
setHead(node);
// 设置p.next=head.next为null,方便GC
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
执行完后线程二获取锁,而线程二也变为head
节点,之前的head
节点被空置,等着被垃圾回收,此时AQS
中队列数据如下:
此时 线程二获取锁成功,并且之前的head
节点会被垃圾回收掉。
线程二释放锁/线程三加锁
当线程二释放锁时,会唤醒被挂起的线程三,流程和上面大致相同,被唤醒的线程三会再次尝试加锁,具体代码就不再分析了,此时AQS
中队列数据如图:
总结
这里用了一步一图的方式来展示了ReentrantLock
的实现方式,而ReentrantLock
底层就是基于AQS
实现的,所以我们也对AQS
有了深刻的理解。
由于篇幅原因,还有很多细节没有讲到,比如ReentrantLock
公平锁的实现,可重入锁的实现,Condition
的实现等,当然大家可以依照着我这种分析模式去尝试一步步画图解析,相信这些细节也都能一步步破解。
后面我还会介绍ReentrantReadWriteLock
的实现原理,仍然使用一步一图的模式来讲解,敬请期待。