目录
2.2.2 tryLock(long timeout, TimeUnit unit)
1.前置了解
AQS 是一个抽象队列同步器,本质上就是一个抽象类,它是JUC下大量工具类的基础类,比如 ReentrantLock、CountDownLatch、Semaphore、线程池等等,都用到了 AQS。
它里面有几个重要的成员属性:
- 由 volatile 修饰的,基于 CAS 修改的 state 属性
- 由 Node 对象组成的双向链表
- 由 Node 对象组成的单向链表(位于 Condition 内部类中)
- 链表头指针,和尾指针
双向链表中 Node 节点的组成:
static final class Node {
// 共享锁 & 互斥锁
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
// waitStatus 属性对应的取值
static final int CANCELLED = 1; // 节点取消了
static final int SIGNAL = -1; // 代表当前节点的后继节点,可以挂起线程,后续我会唤醒我的后继节点
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
// 使用该节点排队的线程
volatile Thread thread;
}
2.深入 ReentrantLock 锁
ReentrantLock 加锁主要有这几种:
- Lock()
- tryLock()
- tryLock(long timeout, TimeUnit unit)
-
lockInterruptibly()
2.1 深入 lock
public void lock() {
// 调用抽象类 sync 的抽象方法 lock
sync.lock();
}
sync 的 lock 有非公平锁和公平锁两种实现方式:
// 非公平锁
@ReservedStackAccess
final void lock() {
// 上来就先基于CAS的方式,尝试将state从0改为1
if (compareAndSetState(0, 1))
// 获取锁资源成功,会将当前线程设置到exclusiveOwnerThread属性,代表是当前线程持有着锁资源
setExclusiveOwnerThread(Thread.currentThread());
else
// 尝试获取锁资源
acquire(1);
}
// 公平锁
final void lock() {
// 尝试获取锁资源
acquire(1);
}
此处的 exclusiveOwnerThread 属性是 ReentrantLock 父类的父类的属性,类似于 synchronizad 里面的 owner 属性。
2.1.1 acquire
该方法公平锁和非公平锁的逻辑一样
public final void acquire(int arg) {
// tryAcquire: 再次查看,当前线程是否可以尝试获取锁资源
if (!tryAcquire(arg) &&
// 1.走 acquireQueued 说明没有拿到锁资源
// 2.addWaiter:将当前线程封装为 Node 节点,插入到双向链表中
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 中断线程的操作(tryLock 和 lockInterruptibly 方法才关心)
selfInterrupt();
}
2.2.2 tryAcquire
tryAcquire 分为公平锁和非公平锁两种实现:
// 非公平锁
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程的 state
final Thread current = Thread.currentThread();
int c = getState();
// --------------- 1.之前持有锁的线程突然释放锁了 ----------------------
if (c == 0) {
// 再次抢一波锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// --------------- 2.判断是否是锁重入 ----------------------
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
// 锁重入的次数超过 int 正数的取值范围了
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 公平锁
protected final boolean tryAcquire(int acquires) {
// 获取当前线程的 state
final Thread current = Thread.currentThread();
int c = getState();
// --------------- 1.之前持有锁的线程突然释放锁了 ----------------------
if (c == 0) {
// a.链表无节点 b。排在第一位,就可以抢锁
if (!hasQueuedPredecessors() &&
// 再抢一波锁
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// --------------- 2.判断是否是锁重入 ----------------------
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 查看是否有线程在AQS的双向链表中排队
// 返回false,代表没人排队或者当前线程排在第一位
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 当前双向链表无节点
return h != t &&
// 当前双向链表有节点,当前节点是不是排在双向链表第一位
((s = h.next) == null || s.thread != Thread.currentThread());
}
2.2.3 addWaiter
该方法的作用:将当前节点加入到 AQS 双向链表中
// addWaite:将没有拿到锁资源的线程扔到AQS队列中排队
private Node addWaiter(Node mode) {
// 封装 node 节点
Node node = new Node(Thread.currentThread(), mode);
// 拿到尾结点
Node pred = tail;
if (pred != null) {
// 当前节点的prev指向尾结点
node.prev = pred;
// 以CAS的方式,将当前线程设置为 tail 节点
if (compareAndSetTail(pred, node)) {
// 将之前尾结点的next指针指向当前节点
pred.next = node;
return node;
}
}
// 如果CAS失败或者尾节点为空,
// 就以死循环的方式,保证当前线程的Node一定可以加入到 AQS 队列的末尾
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
// 拿到尾结点
Node t = tail;
if (t == null) {
// 双向链表无节点
// 构建一个虚拟节点,作为 head 和 tail
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 双向链表有节点
// 以CAS的方式,将当前节点插入到 AQS 队列的末尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
假设 AQS 已有双向链表如下:
此时要将 D 线程加入双向链表,步骤如下:
2.2.3 acquireQueued
该方法的作用:判断当前线程是否还能再次尝试获取锁资源,如果不能,或者又没有获取到,就尝试将当前线程挂起
final boolean acquireQueued(final Node node, int arg) {
// 获取锁资源是否失败 (true:失败 false:成功)
boolean failed = true;
try {
// 中断暂时不考虑,tryLock 和 lockInterruptibly 用到
boolean interrupted = false;
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 前驱节点是头节点,说明当前节点排在第一位,就尝试抢一波
if (p == head && tryAcquire(arg)) {
// 获取锁资源成功
// 设置头节点为当前获取到锁资源的节点,并取消 thread 信息
setHead(node);
// // help GC
p.next = null;
failed = false;
return interrupted;
}
// 没拿到锁资源
// 根据上一个节点的状态,判断当前节点是否可以挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
// 调用 unsafe 类中的 park 方法,将当前线程挂起
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 在 lock 方法中基本不会执行
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// waitStatus
// 1 (CANCELLED) 表示当前节点被取消了
// 0 表示默认状态
// -1 (SIGNAL) 表示当前节点的后继节点,挂起了
// -2 (CONDITION) 表示当前节点在Condition队列中 (调用了 await 方法)
// -3 (PROPAGATE) 表示当前节点是共享锁,唤醒时,后续节点依然需要被唤醒
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前驱节点为 -1,当前节点就可以安心挂起
return true;
if (ws > 0) {
// 上一个节点被取消了,就循环往前找到一个状态 != 1 的节点
do {
// 将当前节点的前驱节点,指向上一个节点的上一个节点 (状态 != 1 的 node)
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 将找到的状态 != 1 的节点的 next 指针指向当前节点
pred.next = node;
} else {
// 上一个节点状态正常,不是 -1 ,也不是 1,就基于 CAS 将其改为 -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 将线程挂起
LockSupport.park(this);
return Thread.interrupted();
}
private void setHead(AbstractQueuedSynchronizer.Node node) {
head = node;
node.thread = null;
node.prev = null;
}
当前节点的前驱节点是头节点,成功获取到锁,进入 if(p == head && tryAcquire(arg)) 的逻辑:
2.2 深入 tryLock
2.2.1 tryLock()
这个方法其实就是 tryAcquire 里面非公平锁的实现,所以 tryLock() 不分公平与非公平,它都是非公平实现
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程的 state
final Thread current = Thread.currentThread();
int c = getState();
// --------------- 1.之前持有锁的线程突然释放锁了 ----------------------
if (c == 0) {
// 再次抢一波锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// --------------- 2.判断是否是锁重入 ----------------------
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
// 锁重入的次数超过 int 正数的取值范围了
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2.2.2 tryLock(long timeout, TimeUnit unit)
调用 sync 抽象类的 tryAcquireNanoc 方法
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 中断标记位从 false 改为了 true,抛异常,让调用者自行处理
if (Thread.interrupted())
throw new InterruptedException();
// tryAcquire 分为公平锁和非公平锁两种实现方式,如果拿锁成功,直接 return
return tryAcquire(arg) ||
// 如果拿锁失败,等待指定时间
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 拿锁失败,直接走人
if (nanosTimeout <= 0L)
return false;
// 设置结束时间
final long deadline = System.nanoTime() + nanosTimeout;
// 将当前节点加入到双向链表中
final Node node = addWaiter(Node.EXCLUSIVE);
// true:拿锁失败 false:拿锁成功
boolean failed = true;
try {
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头节点,直接抢一波
if (p == head && tryAcquire(arg)) {
// 拿锁成功,就设置新的头,断开前驱节点
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 结算剩余的可用时间
nanosTimeout = deadline - System.nanoTime();
// 判断时间是否用尽
if (nanosTimeout <= 0L)
return false;
// 基于前驱节点,判断当前节点是否可以挂起
if (shouldParkAfterFailedAcquire(p, node) &&
// 如果剩余时间太少, <= spinForTimeoutThreshold,就不用挂起了
nanosTimeout > spinForTimeoutThreshold)
// 剩余时间足够,将线程挂起剩余时间
LockSupport.parkNanos(this, nanosTimeout);
// 如果线程醒了,判断是中断唤醒,还是时间到了自然醒
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
// 执行条件:1.剩余时间用尽 2.中断唤醒线程
if (failed)
cancelAcquire(node);
}
}
此处的 tryAcquire 方法和 lock 中的 tryAcquire 是一样的
doAcquireNanos 方法和前边的 acquireQueued 逻辑类似,只不过加上了时间限制
2.2.3 cancelAcquire
cancelAcquire 方法用于将当前节点脱离出AQS双向链表,
为什么 1.剩余时间用尽,中断唤醒线程时,要取消节点 ?
- 如果超时,不取消节点,其他线程可能会尝试唤醒它,但它已经 不再等待锁,导致 多余的 CPU 资源浪费,可能还会阻塞其他线程的唤醒路径,使等待队列的调度逻辑异常
- 如果节点中断唤醒,不取消节点,下一个线程在唤醒时,可能会发现它的前驱节点是一个 无效的节点(已经被中断),需要额外的处理逻辑
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 找到一个状态 != 1 的前驱节点,并把 prev 指针指向这个正常的前驱节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 正常的前驱节点的 next 指针
Node predNext = pred.next;
// 设置当前节点状态为 -1 (取消节点)
node.waitStatus = Node.CANCELLED;
// ----------------------- 1.取消节点是尾节点 --------------------------
if (node == tail && compareAndSetTail(node, pred)) {
// 将 tail 节点替换为上一个节点
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// -------------------- 2.取消节点是中间节点 ------------------------
if (pred != head &&
// 保证前驱节点状态 == -1,如果不是取消节点,就改为 -1
((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 {
// ---------------- 3.取消节点的前驱节点是头节点 -------------------
unparkSuccessor(node);
}
// 自己指向自己 help GC
node.next = node;
}
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// cancelAcquire 方法已经将 ws 置为 1,取消节点进不来
// 所以当前判断是用于持有锁的线程释放锁时,重置 ws
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 找到下一个 ws < 0 的,离当前节点最近的有效节点
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);
}
1.取消节点是尾节点
2.取消节点的前驱节点是头节点
直接从 tail 开始往前找,唤醒离取消节点最近的有效节点
(从后往前找与双向链表的 addWaiter 方法有关)
此处并没有将取消节点从双向链表中断开,只是将它的 next 指向自己,那么这种情况取消节点后,如何被 GC 回收呢 ?
注意这段代码(也就是取消节点是中间节点的逻辑)
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);
1. 虽然取消节点的前驱节点是头节点时,并没有直接处理取消节点,让其被 GC 回收,但是下一次取消节点是中间节点时,例如上图 C 节点被取消时,它会走上述逻辑,找到一个有效的前驱节点(虚拟节点),指向它的后继节点(D节点), 这一步操作可以让 head 节点无法找到这个取消节点
2.下一次循环,D可以抢锁,它会进入 acquireQueued 下面的 shouldParkAfterFailedAcquire 方法中的这个逻辑,从而将 D 的 prev 指针指向虚拟节点,这个时候无论是从 head 节点,还是从 tail 节点开始找,都无法找到 B,C 节点,也就能被 GC 回收了
那么再下一轮循环的时候,D 的前驱就是头节点了,就可以抢锁了,抢锁成功就重新设置头节点
if (ws > 0) {
// 上一个节点被取消了,就循环往前找到一个状态 != 1 的节点
do {
// 将当前节点的前驱节点,指向上一个节点的上一个节点 (状态 != 1 的 node)
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 将找到的状态 != 1 的节点的 next 指针指向当前节点
pred.next = node;
被唤醒的节点会在 lock 下面的 acquireQueued 或者 tryLock 下面的 doAcquireNanos 方法中的死循环里面执行下面的代码进行抢锁:
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
3.取消节点是中间节点
2.3 深入 lockInterruptibly
从源码 ReentrantLock 类中 lockInterruptibly 方法深入下去,发现与前面不同的只有 doAcquireInterruptibly 方法
lockInterruptibly 和 tryLock(timeout, unit) 唯一的不同:
lockInterruptibly,拿不到锁资源,就死等,等到锁资源释放后,被唤醒,或者是被中断唤醒
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
// 将当前节点加入到双向链表中
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点是头节点,尝试抢一波
if (p == head && tryAcquire(arg)) {
// 拿到了锁
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
// 根据上一个节点判断当前节点是否可以挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 中断唤醒抛异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
// 中断唤醒,返回true,正常唤醒,返回false
return Thread.interrupted();
}
2.4 深入 unLock
释放锁的流程大致如下
假如此时 A 线程获取到了当前锁资源,并且重入了一次 state = 2,此时 B 和 C 线程在 AQS 双向队列中排队:
====================================================
1.线程 A 释放锁资源,调用 unLock 方法,执行 tryRelease 方法
2.判断是不是线程 A 持有着锁资源,如果不是,抛出非法监视器状态异常
3.是当前线程持有锁资源, state = state - 1
4.判断 state -1 之后是否为 0, 如果不是,方法结束
5.如果 state - 1 后为 0,就再查看头节点状态是否不为 0,如果为 0,代表后面没有挂起的线程
6.如果为不为 0(为 -1),说双向链表中有挂起的线程,需要唤醒
7.唤醒线程时,先将当前状态重置为 0,再找到有效节点唤醒
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 核心方法
if (tryRelease(arg)) {
Node h = head;
// 头节点不为空,有排队的
// ws 不为 0 (为 -1),有排队的,且有挂起的
if (h != null && h.waitStatus != 0)
// 唤醒离头节点最近的有效节点
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// state - 1
int c = getState() - releases;
// 是否是当前线程持有锁,如果不是,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 当前线程持有的锁是否释放干净
boolean free = false;
if (c == 0) {
// 释放干净,将持有线程置为 null
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}