前置知识 —— AQS
AQS 的全称是 AbstractQueuedSynchronizer(抽象队列同步器),是基于 FIFO 队列实现的,并且内部维护了一个状态变量 state,通过原子更新这个状态变量 state 即可以实现加锁解锁操作。
简单点来说就是加锁失败就会进入一个先进先出的队列中阻塞等待,如果锁被释放会唤醒队列中等待的线程去重新执行加锁逻辑。
内部类与属性
内部类 Node:
static final class Node {
// 标识节点是共享模式下的节点
static final Node SHARED = new Node();
// 标识节点是独占模式下的节点
static final Node EXCLUSIVE = null;
// 标识线程已取消
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;
// 下一个等待在条件上的节点(Condition 锁时使用)
Node nextWaiter;
//......
}
典型的双链表结构,节点中保存着当前线程、前一个节点、后一个节点以及线程的状态等信息。
主要属性:
// 队列的头节点
private transient volatile Node head;
// 队列的尾节点
private transient volatile Node tail;
// 控制锁的状态变量,等于 0 时表示没有线程占有锁,等于 1 时表示已经有人占用锁
private volatile int state;
需要子类实现的方法
AQS 本质上是一个抽象类,说明它本质上应该是需要子类来实现的,它提供了以下方法给子类去实现:
// 独占模式下使用:尝试获取锁
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 独占模式下使用:尝试释放锁
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
// 共享模式下使用:尝试获取锁
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
// 共享模式下使用:尝试释放锁
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
// 判断当前线程是否独占着锁
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
ReentrantLock(独占锁)
内部类与构造方法
内部类:
// 通过内部类 Sync 继承 AbstractQueuedSynchronizer,实现了 AQS 的部分方法
// 一般使用 AQS 都是基于一个内部类来操作
abstract static class Sync extends AbstractQueuedSynchronizer {}
// 用于非公平锁的获取
static final class NonfairSync extends Sync {}
// 用于公平锁的获取
static final class FairSync extends Sync {}
构造方法:
// 默认构造方法,默认使用非公平锁,因为效率高
public ReentrantLock() {
sync = new NonfairSync();
}
// 根据参数选择公平锁还是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
非公平加锁
public void lock() {
sync.lock();
}
逻辑很清晰,如果你用的是非公平模式,会调用 NonfairSync 类的 lock 逻辑,代码如下:
final void lock() {
// 通过 CAS 尝试加锁,如果 state 能够从 0 更新为 1,说明加锁成功
if (compareAndSetState(0, 1))
// 加锁成功,把当前线程设为独占线程,锁的重入性用到
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 加锁失败,进入获取锁的逻辑
}
acquire 方法是 AQS 的核心方法,用于获取锁逻辑,代码如下:
public final void acquire(int arg) {
// 尝试获取锁 && 获取锁失败,进入阻塞队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果线程在等待过程中被中断,会调用 Thread.currentThread().interrupt() 中断逻辑
selfInterrupt();
}
tryAcquire 方法是 AQS 留给子类实现的方法之一,NonfairSync 的实现逻辑如下:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 如果状态变量的值为 0,再次通过 CAS 尝试去获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前加锁线程和持有锁的线程是同一个,发生锁重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
尝试获取锁失败后,就会调用 addWaiter 方法将线程添加到阻塞队列中等待,该方法也是 AQS 的核心方法,代码逻辑如下:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果尾节点不为空,就将当前节点添加到队列尾部
if (pred != null) {
node.prev = pred;
// CAS 更新尾节点为新节点
if (compareAndSetTail(pred, node)) {
// 如果成功了,把旧尾节点的下一个节点指向新节点
pred.next = node;
return node;
}
}
// 如果上面尝试入队新节点没成功,调用enq()处理
enq(node);
return node;
}
当尾节点为 null,即队列都还没有初始化时,会直接进入 enq 入队逻辑,代码如下:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果尾节点为空,说明队列还没有初始化
if (t == null) { // Must initialize
// 初始化一个空的头节点,尾节点指向空的头结点
if (compareAndSetHead(new Node()))
tail = head;
} else { // 队列已经初始化
// 将当前节点添加到队列尾部
node.prev = t;
// CAS更新尾节点为新节点
if (compareAndSetTail(t, node)) {
// 成功了,则设置旧尾节点的下一个节点为新节点
t.next = node;
return t;
}
}
}
}
典型的双向链表添加操作,就是把当前节点添加到由双向链表组成的的阻塞队列的尾部。
线程节点入队完成,就会调用 acquireQueued 方法,这个方法是尝试让当前节点再来尝试获取锁的,因为阻塞队列中可能就它一个节点在等待。代码逻辑如下:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 当前节点的前驱节点
final Node p = node.predecessor();
// 如果前驱节点为 head 节点,说明你在队列的第一位,有资格去获取锁,
// 调用 tryAcquire 方法去尝试获取锁
if (p == head && tryAcquire(arg)) {
// 获取锁成功后将当前节点设置为新的 head 节点
setHead(node);
// 将旧的 head 节点从链表中删除
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断线程是否需要挂起,如果需要挂起就调用线程挂起逻辑
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果线程发生中断,将中断标志位设置为 true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
如果尝试加锁失败或者暂时没有资格获取锁(前驱节点不是 head 节点),调用 shouldParkAfterFailedAcquire 方法判断节点是否需要挂起,代码逻辑如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 上一个节点的等待状态,默认为 0
// static final int CANCELLED = 1;
// static final int SIGNAL = -1;
// static final int CONDITION = -2;
// static final int PROPAGATE = -3;
int ws = pred.waitStatus;
// 如果等待状态为 SIGNAL(等待唤醒),直接返回 true
if (ws == Node.SIGNAL)
return true;
// 如果前一个节点的状态大于 0,也就是CANCELLED(已取消状态)
if (ws > 0) {
// 把前面所有取消状态的节点都从链表中删除
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果前一个节点的状态小于等于0,则把其状态设置为SIGNAL(等待唤醒)
// CONDITION 是条件锁的时候使用的
// PROPAGATE 是共享锁使用的
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
举例说明:
1、线程1来尝试获取锁时,因为目前没有线程持有锁,所以直接加锁成功;
2、线程2来尝试获取锁时,因为线程1已经持有锁,就会初始化队列,构造一个空的 head 节点,然后执行入队逻辑,因为 head 节点 waitStatus = 0(默认),所以会执行到 else 逻辑,即将 head 节点的 waitStatus 设置为 1(SIGNAL)。
3、线程3来尝试获取锁时,因为线程1已经持有锁,然后回执行入队逻辑,因为它的前驱节点(线程2)的 waitStatus = 0(默认),所以会执行到 else 逻辑,即将线程2节点的 waitStatus 设置为 1(SIGNAL)。
说明:SIGNAL 代表当前节点的后继节点需要被唤醒。
第一次执行 shouldParkAfterFailedAcquire 会将前驱节点的 waitStatus 设置为 1,然后返回 fasle,但是 acquireQueued 方法的逻辑是一个 for 循环,所以它第二次执行 shouldParkAfterFailedAcquire 方法时会直接进入 if (ws == Node.SIGNAL) 这个判断,直接返回 true。
返回 true 后,就会执行 parkAndChecknIterrupt 方法,调用 LockSupport.park() 挂起当前线程。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
// 返回是否已中断
return Thread.interrupted();
}
acquireQueued 方法还有一段 finally 的逻辑,当节点被取消时,就会调用 cancelAcquire 将节点移出阻塞队列,代码逻辑如下:
private void cancelAcquire(Node node) {
if (node == null)
return;
// 因为已经取消排队,将 node 内部关联的当前线程置为 null
node.thread = null;
// 获取当前取消排队 node 的前驱节点
Node pred = node.prev;
// 如果前驱节点也处于 CANCELED 状态,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 前驱节点的后继节点,可能是当前 node,也可能是 CANCELED 状态的节点
Node predNext = pred.next;
// 将当前 node 状态设置为 CANCELED
node.waitStatus = Node.CANCELLED;
// 当前 node 是队尾节点 && 修改 tail 的指向当前 node 的前驱节点
if (node == tail && compareAndSetTail(node, pred)) {
// 前驱节点的后继指向 null,完成 node 出队逻辑
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 当前 node 的前驱节点不是 head &&
// 前驱节点状态是 Signal || (前驱节点状态为默认或者 Signal && 设置前驱节点的状态为 Signal)&&
// 前驱节点绑定的线程不为 null
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
// 当前 node 的后继节点不为 null 并且它的状态不是 CANCELED
if (next != null && next.waitStatus <= 0)
// 将当前 node 的前驱节点的后继指向当前 node 的后继节点,完成出队
compareAndSetNext(pred, predNext, next);
} else { // 当前 node 是 head.next 节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
如果当前 node 是 head 的后继节点,则需要唤醒当前节点的后继节点,代码如下:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 将当前节点的状态修改为0,因为它已经唤醒后继节点了,不需要再唤醒了
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 后续解释 || 当前节点的后继节点处于 CANCELED 状态
if (s == null || s.waitStatus > 0) {
s = null;
// 找到离当前 node 最近一个可以唤醒的 node,也可能找不到或者找到的 null
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果找到合适的可以被唤醒的node,则唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
解释下 s == null 的场景:
rivate Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果尾节点为空,说明还未初始化
if (t == null) { // Must initialize
// 初始化头节点和尾节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 如果尾节点不为空
// 设置新节点的前一个节点为现在的尾节点
node.prev = t;
// CAS 更新尾节点为新节点
if (compareAndSetTail(t, node)) {
// 成功了,则设置旧尾节点的下一个节点为新节点
t.next = node;
// 并返回旧尾节点
return t;
}
}
}
}
普通入队时,新节点的 prev 指向 tail,tail 指向新节点,这里后继指向前驱的指针是由 CAS 操作保证线程安全的。而 CAS 操作之后 t.next = node 之前,可能会有其他线程进来。导致在利用 next 指针遍历节点时,可能会出现,节点已经插入即 tail 已经更新,而 prev 的 next 指针依然为 null 的情况。
公平加锁
公平锁在获取锁是也是首先会执行 acquire 方法,只不过公平锁单独实现了 tryAcquire 方法:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果状态变量的值为 0,说明暂时还没有人占有锁
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;
}
如果 state == 0 则代表此时没有线程持有锁,执行 hasQueuedPredecessors() 判断 AQS 等待队列中是否有元素存在,如果存在其他等待线程,那么自己也会加入到等待队列尾部,做到真正的先来后到,有序加锁。具体代码如下:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
返回 false 代表队列中没有节点或者仅有一个节点是当前线程创建的节点。返回 true 则代表队列中存在等待节点,当前线程需要入队等待。
先判断 head 是否等于 tail,如果队列中只有一个 Node 节点,那么 head 会等于 tail。
(s = h.next) == null,这种属于一种极端情况,之前已经讲解过了。
释放锁
public void unlock() {
sync.release(1);
}
调用 Sync 父类 AQS 的 release 方法,代码逻辑如下:
public final boolean release(int arg) {
// 尝试释放锁,由子类实现
if (tryRelease(arg)) {
Node h = head;
// 头节点不为空 && 节点状态不为0
// 在每个节点阻塞之前会把其上一个节点的等待状态设为SIGNAL(-1)
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒下一个等待节点,加锁时讲解过
return true;
}
return false;
}
ReentrantLock 的内部类 Sync 实现了 tryRelease 逻辑,代码如下:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果当前线程不是占有着锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果state == 0,说明完全释放锁了
// 因为可能会存在锁重入情况,需要释放重入次数的锁,其他线程才能获取锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}