图解AQS源码——基于ReentrantLock
AQS
0 锁的基本原理
首先,需要考虑锁的基本组成元素。
- 当前锁的状态,是被持有还是未持有状态
- 如果锁是被持有状态,那么到底是哪个线程持有了当前这把锁
- 锁必须有一个队列去存储阻塞在该锁或者说在等待持有锁的线程
- 锁的实现一定需要考虑怎样阻塞线程,将线程放入到等待队列,以及如何在队列中唤醒等待的线程,这个队列一定是无锁队列【CAS】
1 AQS的父类AbstractOwnableSynchronizer
AbstractOwnableSynchronizer是一个同步器,可能被单个线程排他占用。
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
protected AbstractOwnableSynchronizer() { }
// 持有同步器的线程,解决了上面说的第二个问题,锁被哪个线程持有了
@Getter@Setter
private transient Thread exclusiveOwnerThread;
}
2 AQS-AbstractQueuedSynchronizer
AQS
提供了一个实现阻塞锁以及相关同步器的框架,是大多数同步器的基础。
注意:该框架提供了锁的两个核心元素,即上文所述的1和3
- 基于
FIFO
的线程等待队列 - 当前锁的持有状态
state
子类必须定义改变state
的方法,以及在锁被释放的时候state
代表了什么。一般情况下:
- state = 0 没有线程持有锁,exclusiveOwnerThread=null
- state = 1 有线程持有锁,exclusiveOwnerThread=xx线程
- state > 1 线程重入了锁
那么最后,锁利用什么机制来阻塞和唤醒线程呢?JDK中的Unsafe类提供了阻塞和唤醒线程的一对操作原语,LockSupport
则提供了对于Unsafe
的简单封装、
某个线程获取锁失败,需要思考后续流程怎么处理2
- 将当前线程获取锁设置为失败,获取锁流程结束,但是这样会降低系统的并发度
- 排队等候机制,线程继续等待,仍然保留获取锁的可能性,获取锁的流程仍在继续
- 排队等候机制中的队列应该采取怎样的数据结构
- 排队等待中的线程,什么时候可以再次获取锁
- 如果排队中的线程一直没有办法获取锁,还需要一直等待吗?还是说可以采取别的策略来解决这个问题
// 阻塞的是当前调用的线程
public static void park() {
UNSAFE.park(false, 0L);
}
// 当前线程可以精准唤醒一个线程
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
3 AQS基本原理与数据结构
3.1 原理概览
AQS
使用一个volatile的int类型的成员变量来表示锁状态,通过内置的双向链表队列来支持线程等待,并通过CAS
来完成对state
值的修改。
- 无锁state=0,有锁时state>0第一次加锁
- state=1,如果是可重入锁,加锁state+=1,解锁state-=1,state=0释放锁
- 当持有锁的线程释放锁后,如果等待队列获取到了加锁权限,取出第一个线程去获取锁,获取到锁的线程移出队列
3.2 AQS数据结构
3.2.1 Node
首先看下AQS中定义的最基本的数据结构,Node
static final class Node {
/**
* waitStatus: 节点代表的线程当前等待锁的状态
* 0 初始化后的默认值
* CANCELLED 1 线程的请求已经取消了
* CONDITION -2 当前节点正在一个条件队列中
* PROPAGATE -3 当前线程处于SHARED情况下
* SIGNAL -1 当前线程已经准备好,等待资源释放
*/
volatile int waitStatus;
/**
* 前驱节点
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
* 该节点代表的线程
*/
volatile Thread thread;
/**
* 用于Condition或者共享锁,这里暂不讨论
*/
Node nextWaiter;
}
3.2.2 AQS中定义的字段
/**
* AQS中维护了一个state的字段,代表同步状态,用于展示当前临界资源的获锁的情况。
*/
private volatile int state;
/**
* 头节点,懒初始化,除了初始化以外,只可以通过setHead来修改,状态一定不是CANCELLED。
* 值得注意的是,头节点中没有线程,dummy node
*/
private transient volatile Node head;
/**
* 尾节点,懒初始化,只可以通过enq添加新的等待节点的时候修改
*/
private transient volatile Node tail;
- 无锁state=0,有锁时state>0
- 第一次加锁,state=1
- 可重入锁,加锁state+=1,解锁state-=1,state=0释放锁
- 当持有锁的线程释放锁后,如果等待队列获取到了加锁权限,取出第一个线程去获取锁,获取到锁的线程移出队列
4 以ReentrantLock为🌰看AQS源码
4.1 构造方法
Reentrantlock
支持公平锁
和非公平锁
,默认为非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
sync.lock();
}
4.2 非公平锁
/**
* 非公平锁
*/
static final class NonfairSync extends Sync {
/**
* 非公平锁直接CAS获取锁,获取不到再去排队,调用的是父类AQS的acquire方法
*/
final void lock() {
// 直接获取锁,如果成功,则设置独占锁,获取锁成功
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 没成功,排队
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires)
}
}
// java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
/**
* 非公平获锁
*/
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前同步资源的状态
int c = getState();
// 如果可用,则CAS
if (c == 0) {
if (compareAndSetState(0, acquires)) {
// 将持锁线程设置为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果被占用,如果当前线程已经拥有了该锁
else if (current == getExclusiveOwnerThread()) {
// 线程重入数 + acquires
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 更新锁状态,由于持锁的当前线程,所以可以直接setState
setState(nextc);
return true;
}
// 其他线程持有锁,获锁失败
return false;
}
}
4.3 公平锁
/**
* 分析代码可以得出结论,公平锁是直接去队列中去等待,但是非公平锁会先去尝试直接获取锁
*/
static final class FairSync extends Sync {
final void lock() {
// 调用的是AQS中的acquire方法
acquire(1);
}
/**
* 公平锁tryAcquire
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取锁状态
int c = getState();
// 没有被持有
if (c == 0) {
// 检查是否有前置的等待线程,没有的话获锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果锁被持有了,判断是不是当前线程可重入的
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 判断公平锁是否还需要继续排队
// 0. h == t 头节点等于尾节点,说明未插入新节点,即没有线程获取到锁,返回false
// 1. h != t 头节点不等于尾节点,说明队列中已经有其他线程了,进行下一步判断
// s = h.next 获取头节点的下一个节点
// 假设线程一获取到了锁,头节点的下个节点变成A节点,但是操作是非原子性的
// 2.1 头节点的下一个节点为空,CAS设置tail成功了,可是head.next = node还没有执行完成
// 2.2 头节点的下一个节点不为空,但是不是当前线程,
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
}
这里加一张图解释下hasQueuedPredecessors里面(s = h.next) == null
的逻辑
4.4 获取锁流程分析
4.4.1 线程加入等待队列
// 这个方法是Sync的父类AQS的核心方法 final 模板方法,流程
// 如果当前线程没获取到 tryAcquire(arg) = false
// 那么执行addWaiter(Node.EXCLUSIVE) -> acquireQueued()
public final void acquire(int arg) {
// 这里的获锁方法其实是个模板方法,tryAcquire的具体逻辑在子类实现
if (!tryAcquire(arg) &&
// static final Node EXCLUSIVE = null;
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 如果第一步tryAquire失败了,会执行addWaiter,将当前线程封装成一个node,插入到队尾
// 并将当前的尾节点设置为当前线程节点,并返回当前节点
private Node addWaiter(Node mode) {
// 先利用当前线程和锁模式新建一个节点
Node node = new Node(Thread.currentThread(), mode);
// 先快速CAS插入,不行就一直循环CAS
// 先获取旧的尾部节点
Node pred = tail;
if (pred != null) {
// 旧尾部节点不为空的话,将当前节点的前置节点设置为旧尾节点
node.prev = pred;
// 利用CAS将当前节点为尾部节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 走到这里有两种场景:
// ① pred == null 代表还未初始化
// ② 发生了并发竞争,其他线程设置了新的尾结点,当前线程CAS失败鸟
// 自旋 初始化+CAS设置尾节点
enq(node);
return node;
}
/**
* 返回的是节点的前驱节点
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果尾节点为空,则需要进行dummy node初始化 head = tail = new Node()
if (t == null) {
if (compareAndSetHead(new Node()))
// 注意:这里不是原子操作,会出现短暂的tail != head
tail = head;
} else {
// 如果不为空,说明已经初始化过了,自旋设置尾部节点为当前线程节点
node.prev = t;
// 这里不是原子的
if (compareAndSetTail(t, node)) {
// 这里也不是原子的,可能出现旧的tail的next指针还是指向null的场景
t.next = node;
return t;
}
}
}
}
4.4.2 acquireQueued再次获锁和判断线程挂起
// 执行到这,代表
// ①没有直接获取到锁 ②线程在等待队列中排队获取锁
// acquireQueued会把队列中的线程不断获取锁,判断是否需要挂起
final boolean acquireQueued(final Node node, int arg) {
// 标记是否成功拿到资源
boolean failed = true;
try {
// 标记等待过程中是否中断过
boolean interrupted = false;
// 自旋,要么获取锁,要么中断
for (;;) {
// 这里尝试获取锁是需要满足条件的
final Node p = node.predecessor();
// 必须前驱节点是头节点,代表当前节点位于队列首位,才会去拿锁
if (p == head && tryAcquire(arg)) {
// 获取锁成功,头指针移动到当前node,清空线程和prev指针
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 这里也比较关键,获锁失败后需要判断是否要将线程挂起,如果需要的话则挂起并记录中断状态
// ① 不是第一个节点
// ② 获锁失败了
// 这里如果返回false,代表前驱节点的状态发生了变化,重新自旋判断下
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
/**
* 判断当前线程是否应该阻塞 需要判断前置节点的状态
* 如果是独占锁,只需要考虑CANCELLED和SIGNAL
*
* CANCELLED:由于超时或者中断导致取消,节点不会流转到其他状态了。
* SIGNAL:当前节点的下个节点正在或将要阻塞,因此当前线程在释放或者取消的时候需要唤醒后继节点
* 值得注意的是,节点的这个状态时后继节点设置的
*
* 举个例子:排队,和前面的哥们打个招呼,兄弟,你进去了或者不想等了,都叫醒我。
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取头节点的节点状态
int ws = pred.waitStatus;
// 说明头结点处于唤醒状态
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
// 删除掉所有的取消节点
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 这里只能是初始状态0了,给前面哥们提个醒
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
4.5 释放锁流程分析
首先看下ReentrantLock提供的的释放锁操作
protected final boolean tryRelease(int releases) {
// 这里考虑重入锁,持锁次数减去releases
int c = getState() - releases;
// 持锁线程非当前线程,直接抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 锁是否free
boolean free = false;
if (c == 0) {
// 释放锁 = 持锁线程为null
free = true;
setExclusiveOwnerThread(null);
}
// 设置扣除releases的state
setState(c);
return free;
}
再来看下AQS中定义的流程模板
public final boolean release(int arg) {
// 如果锁成功释放了,需要唤醒后继节点
if (tryRelease(arg)) {
Node h = head;
// h == null ---> 前面获锁线程都不需要排队,那么直接返回true
// h != null ---> 唤醒后继节点的条件是初始化了
// ws == 0 代表还没有线程进入过等待队列
// ws == -1 后继节点需要被通知,这里的ws必定为-1 SIGNAL
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
// 释放锁,传入的node是head
// 这里会判断ws<0,对于独占锁只用考虑SIGNAL,会将SIGNAL替换成0
// 需要思考下替换成0的意义
// 暂时还没有想出来 todo
compareAndSetWaitStatus(node, ws, 0);
/*
* 唤醒的线程在后继节点中存储,通常情况下都是下个节点
* Thread to unpark is held in successor, which is normally just the next node.
*
* 这里值得注意的点是这段话,翻译下就是:如果后继节点为空或者被取消了,那么会从后往前去找没有被取消的后继节点
* But if cancelled or apparently null, traverse backwards from tail to find the actual non-cancelled successor.
*/
Node s = node.next;
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;
}
if (s != null)
LockSupport.unpark(s.thread);
}
那么问题来了,为什么从后向前呢?
还是使用之前的那张图,因为之前线程添加到等待队列的操作是非原子的,存在中间态,假设tail
已经变成了其他线程,但是此时head.next
指向的还是null
,而prev
是正确的,所以对于当前线程来说,从后向前是一定可以遍历到当前所有的等待线程的。
if (pred != null) {
// 第一步成功了
node.prev = pred;
// 第二步CAS成功设置了tail
if (compareAndSetTail(pred, node)) {
// 第三步,设置前驱节点的后继节点,这一步可能没完成
pred.next = node;
return node;
}
}
4.6 CANCELLED状态节点生成
private void cancelAcquire(Node node) {
if (node == null)
return;
// 设置该节点不关联任何线程,虚节点
node.thread = null;
// 跳过取消状态的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 获取过滤后的前驱节点的后继节点
Node predNext = pred.next;
// 将当前节点的状态设置为CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果当前节点是尾节点,将尾节点从当前节点设置为当前节点的前驱结点
// 如果tail更新成功,更新tail的next为null
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// 更新失败的话,
// 1.如果当前节点的前置节点不是head节点
// 2.1 前置节点的ws为Node.SIGNAL
// 2.2 ws<=0 且 将前置节点的ws设置为Node.SIGNAL成功
// 3 前置节点的线程不为空
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// 如果当前节点的下个节点设置为前驱节点的后继节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 如果当前节点是head的后继节点 或者 上述条件均不满足,唤醒当前节点的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}