AQS源码解析
简介
AQS全称是AbstractQueuedSynchronizer(抽象队列同步器).,是jdk juc包下的一个类,juc包下很多类都频繁用到了这个类的锁的思想和设计,我将结合ReentrantLock类并且按照非公平锁(公平锁和非公平的区别后面详细介绍)的方式来讲解AQS的设计思路和源码分析。接下来让我们一起看看吧!
Aqs
Aqs类主要有两个方法,分别是acquire以及release。在详细深入解读源码之前需要先介绍一下Aqs的工作原理,后续有不明白的点可以回过来先看看整体流程。先介绍一些基本概念:
- state:是一个使用volatile修饰的int类型变量,Aqs通过该变量来控制锁的状态,并发场景下通过volatile+cas 来保证原子性
- 同步等待队列:Aqs维护了一个双向链表,链表结构Node包含prev(前驱指针),next(后继指针),thread,waitStatus等属性.队列用于处理线程阻塞唤醒,争抢锁等事情。waitStatus包含的状态:
- int CANCELLED = 1 (取消,该节点不需要再进行处理).
- int SIGNAL = -1(唤醒,该节点等待唤醒,同时该节点.next可以进行安全阻塞).
- int CONDITION = -2(条件队列,先暂时不讨论)
- PROPAGATE = -3(传播级别,这个级别的线程可以进行唤醒)
acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 尝试获取锁,有公平/非公平两种方式
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // addWaiter: 把当前线程封装成Node加入到同步等待队列中.acquireQueued:对队列中的当前节点进行安全阻塞
selfInterrupt();
}
其中几个核心方法:tryAcquire、addWaiter、acquireQueued
tryAcquire为尝试获取锁,addWaiter是把当前线程封装成节点加入到等待同步队列中,acquireQueued是尝试让队列首元素抢锁,并且抢失败后处理阻塞等事务.这里的逻辑是尝试抢锁,抢失败的话把当前线程封装成Node节点加入到同步等待队列中,并且对队列中元素尝试拿锁,失败后阻塞.
tryAcquire方法
获取到state,判断当前锁状态,如果state为0说明锁空闲,则当前线程可以抢锁,则通过cas设置state为1,如果成功了说明抢到了锁,ReentrantLock为独占锁,所以还会把独占线程exclusiveOwnerThread设置为当前线程。如果当前锁已经被抢了,但是当前独占现场为当前线程的话,说明当前线程正在进行锁重入的过程,那么同样cas设置state,只是state此时代表的是锁重入的次数
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 通过cas设置state,抢锁
setExclusiveOwnerThread(current); // 独占式的锁有线程标记,抢到锁则标记为当前线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 可重入锁的情况下,state表示重入次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
当tryAcquire方法返回失败,说明当前线程没有抢到锁,那么就准备进行阻塞相关的事宜了.
####addWaiter方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) { // 队列已经初始化过了就直接链接在后面
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); // 初始化同步等待队列,初始化完后的结构:head->node(tail)
return node;
}
代码逻辑:把线程封装成Node加入到等待队列中.其中enq是首次进入时,会进行队列的初始化,而第一个if条件则是之后进入时会进到的.
enq方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node())) // cas设置head、tail
tail = head;
} else { // 自旋后把node接在队列后面,tail也同步移动
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这一段通过自旋来保证队列的初始化一定成功.首次进入会通过cas设置队列头head和尾tail,第二次自旋则会把节点接到head后面,tail也移动到队列尾部.整体结构:head->Node(tail).(实际是双向链表,这里只是简单表示)
acquireQueued方法
参数node:当前线程通过addWaiter方法包装后得到的节点.
首先这里通过for循环不断的自旋,然后取到node的前驱节点p(node.prev),
条件p==head意思是当前节点node是head.next,即node为队首第一个节点,如果是队首节点则通过tryAcquire再次尝试抢锁.抢到锁的话当前节点进行出队操作,没抢到则进入shouldParkAfterFailedAcquire方法判断是否可以安全阻塞,如果可以安全阻塞则调用parkAndCheckInterrupt阻塞当前线程
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false; // 中断标识
for (;;) {
final Node p = node.predecessor(); // node.prev node的前驱节点
if (p == head && tryAcquire(arg)) { // 如果node是队首节点,则尝试获取锁,如果成功获取到就出队
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted; // 返回当前线程的中断标识
}
if (shouldParkAfterFailedAcquire(p, node) && // 判断当前线程是否可以安全阻塞,如果可以安全阻塞则阻塞线程,返回中断标识
parkAndCheckInterrupt()) // 阻塞
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire方法
首先取到node.prev的等待状态waitStatus,后续的操作是基于状态来判断的.
1、如果node的前驱节点的状态为SIGNAL(-1),则表示当前节点可以安全阻塞,则返回true.
2、如果前驱节点状态大于0,则说明状态为CANCELLED(1),即取消,该节点则需要在队列中进行剔除处理
3、如果前驱节点状态为0,cas设置为SIGNAL(-1)
如果第一个线程进来前驱节点waitStatus为0,则先cas改成SIGNAL(-1),然后自旋再进入之后,前驱节点为SIGNAL的情况下,当前节点被视作可安全阻塞,返回true
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // 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; // 剔除掉状态为CANCELED的节点,从后往前
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 处理前驱节点状态,先暂时不考虑CONDITION状态,这里就处理0或者PROPAGATE状态,0和传播状态都可以唤醒
}
return false;
}
parkAndCheckInterrupt方法
调用LockSupport.park来阻塞当前线程.
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
release方法
包含tryRelease、unparkSuccessor两个方法
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放锁
Node h = head;
if (h != null && h.waitStatus != 0) // 释放锁成功了则对同步等待队列进行唤醒.status应该要为Signal
unparkSuccessor(h); // 唤醒队首节点或等待队列中其他有效节点(队首无效的情况下)
return true;
}
return false;
}
tryRelease方法
释放锁,通过对state的减操作来完成.如果state减到0了说明锁已经释放了,则返回true,并且设置独占现场为null
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 如果state减为0,说明锁空闲了,则释放锁成功
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor方法
该方法进入的条件是成功释放锁,且head!=null,且head的状态为-1。
该方法逻辑是对node节点的状态进行判断.对CANCELD(1)状态的节点同样进行剔除,并且通过LockSupport.unpark(s.thread)唤醒队首节点(如果队首节点无效会在队列中继续找)
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)
compareAndSetWaitStatus(node, ws, 0); // 把head的status改成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) { // 如果队首节点无效,即空或者为CANCELED状态,则剔除掉
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); // 唤醒当前节点
}
如果有帮助到你可以点个赞哦~~
如果有什么不明白的可以在评论区留言,我会加以改正,你的评论可以帮助到更多人~