文章目录
AbstractQueuedSynchronizer (AQS) 是什么?
本文主分析独占锁源码,共享锁源码分析转到 AbstractQueuedSynchronizer(AQS) 之共享锁源码浅读
1.是用来构建锁或者其他同步器组件的重量级框架,是整个 JUC 底层实现,内置 FIFO
队列完成资源获取和排队工作,并通过一个 int
变量来表示持有锁状态。
2.是提出锁的一种规范,屏蔽同步状态的管理,阻塞线程排队和通知,唤醒机制。
3.加锁就会阻塞线程,有阻塞就会产生排队,有排队就需要队列,既然说到排队,那么就一定需要有某种队列形成,这样的队列是什么数据结构呢?
AQS 数据结构(CLH 队列)
如果共享资源 state
被占用,就需要一定的阻塞唤醒机制来保证锁分配,这个机制主要用的是 CLH
队列变体实现的,将暂时获取不到锁的线程加入到 CLH队列
中,这个队列就是 AQS
的抽象表现,它将请求共享资源的线程封装到了队列的 Node
节点中,通过 CAS
、自旋
以及 LockSupport.park()
方式,维护 AQS 中的变量 state
状态,使并发达到同步的效果。如下图示:
CLH(Craig,Landin and Hagersten)是三个人,共同发明了一个可扩展、高性能、公平且基于自旋锁的链表
上述图中展示了 AQS 的框架结构,下面在继续查看该类内部相关信息。
AQS 类结构及常用 API
AbstractQueuedSynchronizer
是一个抽象类(既然是抽象类,肯定提供了模版方法要让我们自己实现的啦),先来看看有哪些成员(摘出的是 AQS 主流程需要用到的成员信息),如下所示:
public abstract class AbstractQueuedSynchronizer {
// AQS 同步器中指向头结点的引用
private transient volatile Node head;
// AQS 同步器中指向尾节点的引用
private transient volatile Node tail;
// 线程共享资源
private volatile int state;
// Node 内部类封装Thread和线程状态,以及锁是独占锁还是共享锁
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;
// 线程的状态,初始值是 0
volatile int waitStatus;
// Node 的前序节点
volatile Node prev;
// Node 的后驱结点
volatile Node next;
// 当前线程
volatile Thread thread;
}
}
AQS 类组成结构:
- 从上述代码中可以看到有四个非常重要的属性(
head
、tail
、state
、内部类Node
),AQS 类设计理念是队列,遵循 FIFO 原则,所以自然而然肯定有head
,tail
两个头尾节点,然后共享资源state
,还有个Node
内部类组成 AQS 抽象类。 - 父类
AbstractOwnableSynchronizer
类中还有一个属性exclusiveOwnerThread
,用来存储加锁成功的线程引用。
Node 内部类组成结构:
Node
里封装的是Thread
线程waitStatus
表示线程的状态(默认初始值是0、还有CANCELLED(1)
、SIGNAL(-1)
、CONDITION(-2)
、 -PROPAGATE(-3)
总共五种状态)prev
前驱节点next
后驱节点mode
表示是要加独占锁还是共享锁(默认独占锁EXCLUSIVE
)
由此可以看出 AQS 是由上述两大部分组成,里面的内部类 Node
其实是一个双端链表。
因为 AQS 是个抽象类,必不可少的肯定要儿子类去帮他完成一些事情,所以除了上面的属性之外还提供了4个常用 API 如下:
tryAcquire(int arg)
// 尝试加独占锁tryAcquireShared(int arg)
// 尝试加共享锁tryRelease(int arg)
// 尝试释放独占锁tryReleaseShared(int arg)
// 尝试释放共享锁
总结起来:同步器中有一个 state
变量控制锁的装态,同时还要知道哪个线程作为 head
,tail
节点,外层就只有这三个重要结构,内部有个 Node
类封装了线程以及状态,Node 结构还是一个双向链表,pre
,next
节点的指向前后的 Node
节点。
总结流程图如下:
AQS 源码分析
AQS 本身是一个抽象类,里面的方法都是空方法,所以我们从它的著名子类 ReentrantLock
开始分析,先来看一个最简单熟悉不过的代码,如下:
public class LockSupportDemo {
public static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
// ...doSomething
lock.unlock();
}
}
进入 lock
内部方法,如下:
public void lock() {
sync.lock();
}
发现调用的是 sync.lock()
,那这个 sync 是啥玩意呢?继续查看 sync 代码如下:
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
}
}
发现 ReentrantLock
底层实际用的是 AQS,自己写了个内部类叫 Sync
,继承 AQS,覆写 AQS 中提供的API :
tryAcquire(int arg)
// 尝试加独占锁tryAcquireShared(int arg)
// 尝试加共享锁tryRelease(int arg)
// 尝试释放独占锁tryReleaseShared(int arg)
// 尝试释放共享锁
继续进入 sync.lock()
内部发现他有两个子类实现,如下:
发现是我们平常所说的公平锁
,和非公平锁
,我们这里先看非公平锁
NonfairSync
,代码如下:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 调用的子类 NonfairSync 里的 lock 方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
发现 NonfairSync
又是 Sync
的子类,调用 lock
方法,内部可以看到是使用 CPU 原语指令 CAS 操作。进来就开始尝试 CAS 加锁,加锁成功返回,失败走 acquire(1)
,因为第一次进来,state
默认值是 0,执行这条指令 compareAndSetState(0, 1)
肯定是成功的,然后加锁成功的线程会把自己的大名贴上,表示占用这把锁,在没有释放之前,其他线程不能加锁(因为是独占锁),这里就假设第一次获取到锁的线程一直占着这把锁不释放,后面都是针对这个场景去分析,假设这个线程一直持有锁就行了,继续往后面分析。
但是如果此时其他线程过来抢夺这把锁,也就是想修改 state
的值,因线程1没释放锁,就只能走 else
操作执行 acquire(1)
, 进入 acquire(1)
内部(其实就是没有加锁成功的线程都会被 AQS 进行接管),代码如下:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
会发现 AQS 是采用的模版设计模式,发现有三部分构成tryAcquire()
、acquireQueued()
、addWaiter()
。tryAccquire()
方法 AQS 没有实现,子类 ReentrantLock
自己去实现,后面的 addWaiter()
、acquireQueued()
方法都是 AQS 内置方法,不需要程序员去管理的。先进入到 tryAccquire()
方法内部,代码如下:
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
}
线程过来先调用 AQS 提供的 getState()
方法取出内存中 state
值,判断是否为0 ,0 表示没有被人修改过,非 0 肯定是有其他线程修改过的,很显然,线程1没释放锁,state
还是 1,现在过来的这个线程肯定是不能加锁成功的。然后判断 else if
逻辑,else if
中的逻辑是判断可重入锁的逻辑,就是判断当前线程和 加锁成功的线程是否一致,如果一致那就给 state
加 1,释放的时候也要减1,直到减到 0 才算释放成功,这就是可重入独占锁的实现方式。很显然当前线程肯定不是 AQS 上加锁成功的线程,所以当前线程加锁失败,判断可重入逻辑也是失败,那就直接返回 false
,我们说过了,所有加锁失败的线程都会被 AQS 接管处理,所以回到上一层代码。
继续执行后面的语句 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
这条语句我们先看最里面的 Node.EXCLUSIV
表示要创建一把独占锁,代码如下:
然后先进入到 addWaiter()
方法,代码如下:
abstract static class Sync extends AbstractQueuedSynchronizer {
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);
return node;
}
}
会发现,这里新建了一个 node
节点(暂时把名字叫做 Node1(此时的 Node1 里面的属性 Thread 为当前线程,假设名字叫做 ThreadB,waitStatus 为默认值 0 ,prev,next 都还是 null)),并把当前线程封装进去,同时设置为独占锁模式。然后 tail
是 AQS 中的属性,用来指向队列的尾节点,默认值是 null
,所以这里 pred = null
,直接走 enq(node)
代码,进入方法内部,代码如下:
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;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
会发现是一个自旋 CAS 操作,线程进入到 for
循环,因为第一次进来 tail = null
,所以会新建一个空 Node 节点(暂时把名字叫做 Node2),里面的属性(prev,next,thread,waitStatus(默认值0))全部都还没有赋值,然后执行代码 compareAndSetHead(new Node())
,表示将 AQS 同步器中的 head
头指针指向新建的空节点(也可称之为哨兵节点),此时 Thread = null
、waitStatus=0
,如下图示:
1.如果没有哨兵节点,那么每次执行入队操作,都要判断
head
是否为空,如果为空则head=new Node
如果不为空则head.next=new Node
, 而有哨兵节点则可以直接建立引用head.next=new Node
。
2.如果没有哨兵节点,可能存安全性问题,当只有一个节点的时候执行入队方法,无法保证tail
和head
不为空。哪怕节点入队前tail
和head
还指向同一个节点,下一时刻可能由于并发性在具体调用enqueue
方法操作tail
的时候,head
和tail
共同指向的头节点已经完成出队,此时tail
和head
都为null
,所以enqueue
方法中的tail.next=new node
会抛空指针异常,这就是哨兵节点添加进去的可以有效避免的问题。
Node1 节点里面的封装着 ThreadB
,waitStatus=0
;Node2 是新建的空节点,Thread=null
, waitStatus=0
。然后再执行代码 tail = head
,表示将 AQS 中尾指针指向哨兵节点,如下图示:
注意,循环还没有结束呢,然后再走一遍逻辑,此时的 tail
肯定是不为 null
的,已经指向了 Node2 哨兵节点,然后执行代码 node.prev = t;
(t是tail
,注意 tail
原来指向的是 Node2 哨兵节点, 所以 t 指向的是原来的尾节点 Node2,node 就是我们传进来的 Node1) 这句代码表示将 Node1 的前驱指针指向 t
, t 原来指向的是 Node2 哨兵节点,如下图示:
然后再执行代码 compareAndSetTail(t, node)
表示把 AQS 的尾指针指向 Node1,队列尾指针肯定是永远指向尾部的。如下图示:
然后再执行代码 t.next = node;
(t是tail
,tail
原来指向的是 Node2 哨兵节点) ,这句代码表示将 Node2 哨兵节点的后驱指针指向 Node1 节点,如下图示:
程序运行到此整个 enq()
方法就算结束,后面就不会再进来了,让我们总结下 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;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 新建空节点(哨兵节点)
- 维护 AQS 和哨兵节点引用(
head
、tail
指向谁) - 维护第一个入队的 Node 和 哨兵节点 Node 关系 (之前我们说过 Node 一个双端链表节点,自然而然要维护前后引用指向谁)
让我们返回调用 enq()
方法处,代码如下:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
看到线程执行完 enq()
方法之后,就把当前的 node
(这里我们上面说了暂时命名为 Node1 节点) 返回出去,这里先不返回出去。先来看看这个 if
里面的逻辑,假设其他线程第三次过来执行这个 addWatier ()
方法,此时传过来的 node
我们称之为 Node3,此时 tail
肯定不为 null
(已经指向了 Node1 节点),所以会进入 if
的逻辑,开始执行 这行代码 node.prev = pred;
表示把 Node3 节点的前驱指针指向 pred
,pred
是谁呢?是原来的tail
,原来 tail
指向的是尾部节点 Node1,上述过程演示过的,尾部节点是 Node1,所以自然而然,这里把 Node3 的前驱指针 prev
指向了 Node1,如下图示:
然后再执行代码 compareAndSetTail(pred, node)
把 AQS 的 tail
尾部尾指针向后移动,指向最后一个节点,也就是最后入队列的节点 Node3 ,如下图示:
执行完之后,在执行代码 pred.next = node;
(pred
原来的尾指针指向的对象,也就是 Node1 节点) 意思是把 Node1 节点的后驱指针 next
指向 Node3,如图示:
最后返回 Node3 节点,假设在这行这行代码 if (compareAndSetTail(pred, node))
的时候失败了,那么就会已进入到 enq()
方法中的第二段逻辑中,代码如下:
会发现 enq()
会一直做自旋 CAS 操作,知道你的 Node 之间的引用关系建立好,才会退出。分析到这里整个 addWaiter()
方法就分析完了,总结 addWaiter()
方法干了哪些事情?
- 第1新建空的哨兵节点,
- 第2建立 CHL 队列 Node 节点之间的引用关系
然后返回代码上一层继续追踪,代码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
继续进入 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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
加锁失败的线程(假设是 ThreadB)过来执行代码 node.predecessor()
表示返回当前节点的前驱节点。
假设1我们现在过来的节点是 Node1 节点,arg
是外面写死的1,那么 Node1 的前驱节点自然而然是 Node2 哨兵节点(由下图可看出)
然后执行在执行代码 if (p == head && tryAcquire(arg))
,发现 p
指向的是 Node2,head
指向的也是 Node2,所以 p==head
条件成立,表示你这个节点是即将第一个出队列的节点,所以紧接着就会让你重新尝试 1 次加锁,也就是执行代码tryAcquire(arg)
,也就是走下面这段代码(前面已经介绍过了,在此不再啰嗦):
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
假设第1种情况:如果现在过来的线程加锁成功,那么就会进入到 if
里面的逻辑,执行代码 setHead(node);
先看看 setHead()
方法内部,代码如下:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
表示把 AQS 的 head
头指针指向了 Node1 节点,其实就是相当于把 Node1 节点出队列了,然后 Node1 节点会被当做新哨兵节点,如下图示:
虚线标识是没有指向了,灰色表示没有任何引用了,可以被 GC 垃圾回收,此时 AQS 的头指针指向了 Node1 节点,Node1 节点会被当做新的哨兵节点,那么此时里面的 Thread
也可以置为空,因为 Node1 节点中的 ThreadB
已经加锁成功了,已经出队列了,然后 Node3 节点也是和 Node1 节点一样加锁成功就出队列。
假设第二种情况:如果过来的线程(ThreadB)加锁失败,就会执行 shouldParkAfterFailedAcquire(p, node)
方法,进入代码如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 默认都是 0
if (ws == Node.SIGNAL) // 挂起等待唤醒的状态 -1
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
Node1 节点过来执行,pred
节点是哨兵节点,此时的 waitStatus=0
,没人修改过,会走到最后一个else
执行代码 compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
表示将哨兵节点的 waitStatus
状态改成 SIGNAL(-1)
状态,如下图示:
其实这个方法就是每次过来的节点会把前驱节点的 waitStatus
修改成Node.SIGNAL(-1)
状态, 假设设置状态成功,那么会返回 false
,回到外层,可以发现是个 for
循环,代码如下:
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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
所以又会重新走一次逻辑,又尝试去执行代码 if (p == head && tryAcquire(arg)
方法,再尝试 1 次加锁看是否能成功,上面其实第一次进来已经尝试过了 1 次,加上这 1 次设置为状态之后又 1 次尝试,算起来总共尝试了 2 次加锁了,然后我们假设它还是加锁失败,那么就会走这行代码 if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())
,进入到 shouldParkAfterFailedAcquire()
内部如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 默认都是 0
if (ws == Node.SIGNAL) // 挂起等待唤醒的状态 -1
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
此时哨兵节点中 waitStatus 已经被赋值为 -1,所以直接会返回 true
,执行完 shouldParkAfterFailedAcquire()
方法,然后再进入到方法 parkAndCheckInterrupt()
内部,代码如下:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
会发现很简单,就两行代码,表示让当前线程挂起,如果没有中断 Thread.interrupted()
返回 false
,并复位也还是 false
,中断场景我们后面分析。此时执行的线程都是停留在这个地方等待着被唤醒,假设现在线程在某个地方被唤醒了,那么就会从这一行开始继续往下执行,假设线程是灭有被修改过中断标识的哈,然后继续执行,又会继续执行 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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
会发现又会过来执行这行代码 if (p == head && tryAcquire(arg))
,又来尝试 1 次加锁,这里加上前面的2次尝试,总共是3次,可以总结出来一句话:尝试加锁失败的线程都会给你 3次机会(第一次进入方法 acquireQueued()
为第一次尝试加锁,第二次执行方法 shouldParkAfterFailedAcquire()
修改线程都 waitStatus 状态为第二次,第三次为挂起之后被唤醒又去执行尝试加锁方法为第三次),假设现在唤醒之后尝试加锁成功了,那么就会执行 setHead() 方法,代码如下:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
主要是把当前节点前驱节点置为 null
,然后把AQS 的 head
指针移动到了该 Node 节点上,然后再执行 p.next = null;
表示把原来的哨兵节点的后驱指针置为 null
,此时 Node2 (原来的哨兵节点)即为游离态对象,所以会被 GC 垃圾回收掉,此时 Node1 就被晋级为新的哨兵节点。
然后总结下 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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
对着上述代码总结:
- 多次让线程尝试加锁
- 加锁失败后,挂起/阻塞线程,并修改其
waitStatus
状态为 -1,主要是这个方法修改shouldParkAfterFailedAcquire
状态 - 加锁成功,踢除老哨兵(其实就是相当于出队列了),新哨兵上位(线程加锁成功了,自然而然 Node 节点就不需要保存这个线程相关的信息了)
回到这边,那么这些被挂起被阻塞的线程什么时候被唤醒呢?为什么会被挂起呢?肯定是因为想去尝试加锁,加锁失败了,那么只有等锁要被释放的时候,这些阻塞的线程才会被唤醒然后去竞争尝试加锁,所以这里就需要看释放锁的过程,还是比较简单的。
进入 lock.unlock()
内部,代码如下:
public void unlock() {
sync.release(1);
}
因为现在分析的是独占锁么,所以只会有一个现成成功,自然而然释放锁,也就是只有一个人释放,直接传入1就够了,不过这里提一下,这个 state 状态不一定非要是 1,其含义你可以在自己实现自己业务时自己定义,再进入 release()
方法内部,代码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
这是个模版方法,tryRelease()
肯定要自己实现的,if
里面的逻辑肯定是 AQS 封装的,进入 tryRelease()
方法内部,代码如下:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
会发现释放独占锁还是比较简单,也就是将之前的修改的 state
开始复位操作,然后把占用的坑位清空即可。if (c == 0)
这里面的逻辑是用来把之前可重入锁加锁了几次,那么就要释放几次,知道释放成功,也就是到 0 为止,就算释放成功。
假设现在释放锁成功了,state 装填变成 0了,资源被释放出来了,其他线程就可以开始抢占了,但是其他线程在尝试加锁的过程中已经都被无情的阻塞挂起了,所以释放锁成功,你必须得那几个挂起的抠脚大汉唤醒,所以释放锁成功之后,里面就要开始执行代码 unparkSuccessor()
,猜都能猜到,肯定是取唤醒沉睡的线程,进入代码如下:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
第一眼就看到了 LockSupport.unpark(s.thread);
这行代码就是唤醒阻塞挂起的线程的,但是前面有一堆的判断逻辑,先来看看吧,此时的 node 其实就是 head 指向的哨兵节点,从这段代码可以看出 node
就是 head
头结点,代码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
可以看到释放锁成功之后,就会把 head
赋值给 h
变量作为 unparkSuccessor()
方法的入参,之前在阻塞挂起线程之前我们执行过了一个方法 shouldParkAfterFailedAcquire()
将节点的 waitStatus
修改成了 -1,所以这里 if 判断逻辑都成立,直接进入到 unparkSuccessor()
方法 ,代码如下:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
传进来的 node
也就是 head
头节点,head
指向的也就是哨兵节点,如下图示:
此时的 node.waitStatus
是为 -1 成立,所以这里又会把 node 节点的 waitStatus
修改回为原来的默认值 0,然后执行代码 Node s = node.next;
表示拿到后驱节点,当然就是第一个阻塞挂起的线程 ThreadB,然后执行 LockSupport.unpark(s.thread);
把线程 ThreadB 唤醒,那么唤醒的线程就会在上次阻塞的地方重新醒来,继续往下执行,代码如下:
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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
那么唤醒之后,原来阻塞在那边的线程就要开始复活,然后接着从阻塞线程的地方开始往下分析,代码如下;
parkAndCheckInterrupt()
方法刚才挂起了线程,现在 Node1 线程被唤醒,那么会立即执行么?不一定的哦,因为可能有新建来的线程竞争插队执行的,假设没有线程插队,Node1 唤醒,然后开始往下执行继续走 for
循环,又开始执行第一个if
判断,p==head
成立,尝试加锁也成功,然后把 Node1 作为哨兵节点
,并把 Node1 和原来哨兵节点的引用置 null
。
等待队列中有线程取消或者中断时怎么维护双向链表?
从下面这段代码开始分析,代码如下:
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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
假设有个线程一直持有锁,那么其他线程肯定都要入 CLH 队列中被阻塞挂起,然后等待获取锁,现在假设 CHL 队列中有 3 个阻塞挂起的线程,分别是 Node2、Node3, Node4 ,如下图示:
此时 Node3 因为某些原因(超时退出、中断异常)状态为 CANCELLED(1) 取消状态),表示这个线程不玩了。如下图示:
那么 Node4 肯定肯定不能把自己的前驱节点指向它吧,得向前去寻找到一个不为取消状态的节点作为前驱节点,但是因为执行了方法 addWaiter()
已经建立好了这个 Node3 和 Node4 的关系,所以在 shouldParkAfterFailedAcquire()
方法中有去维护这个异常状态,AQS 中已经帮我们实现了这个逻辑,代码如下:
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 {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
假设现在 Node4 过来执行这个 shouldParkAfterFailedAcquire()
方法,pred
为 Node3, 但是 Node3 线程已经被取消了,此时waitStatus = 1
就会执行 if (ws > 0)
里面的逻辑。会发现这是一个 while
循环,退出条件是 pred.waitStatus > 0
也就是必须找到 waitStatus 为正常状态,也就只有一个 0 状态的,0 又是表示线程初始状态。从上面的图示可以看出也就是找到 Node1 其实就可以,看这句代码 node.prev = pred = pred.prev;
把 Node3 的前驱指针指向了 Node1,可以看出这个 while 循环就是一直往前寻找,知道找到一个不被取消的 Node 节点然后就退出,把 Node3 前驱指向找到的不被取消的 Node。
- static final int CANCELLED = 1;
- static final int SIGNAL = -1;
- static final int CONDITION = -2;
- static final int PROPAGATE = -3;
- 还有一个默认状态 0
如下图示, 灰色表示退出的 Node 节点。
上述方法维护了 CLH 队列的节点取消的情况。
还有另一种极端场景,假设 CLH 队列中的其中一个被阻塞挂起的 Node4 被中断了,被中断了会抛出一些异常并且还强制让它退出了,那么自然而然就会走到 finally
里面,我们看下 finally
里面执行的逻辑,注释写在代码上:
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
// 因为这个阻塞挂起的线程被中断了,所以相当于要退出 CLH 队列
// 所以先把这里面的线程属性赋值为 null
node.thread = null;
// 找到前驱节点,因为假设被中断的是 Node4,所以前驱节点是 Node3
Node pred = node.prev;
// 这里是假设 Node4的前驱节点 Node3也被取消了
while (pred.waitStatus > 0)
// 如果前驱节点 Node3 也取消掉了,那么就要一直往前找,找打一个不被取消的节点作为 Node4 的前驱节点
node.prev = pred = pred.prev;
// 把找到的第一个不被取消的前驱节点的后驱指针存放到一个临时变量 predNext 中
Node predNext = pred.next;
// 把当前自己的 Node 的 waitStatus 设置成取消状态(1)
node.waitStatus = Node.CANCELLED;
// 如果被中断的 Node 本省就是最后一个节点,那么直接把 AQS 的 tail 指向该节点的前驱节点即可
if (node == tail && compareAndSetTail(node, pred)) {
// 把前驱节点的后驱节点置为 null,因为前驱节点已经是最后一个尾节点了
compareAndSetNext(pred, predNext, null);
} else {
// 加入不是尾巴节点,肯定也不是头结点,头结点是哨兵节点,取消这个节点没意义。
// pred 是通过上面 while 找到的第一个不被取消的 Node 节点,所以所有条件都是成立的,直接进入 if 里执行
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// 临时变量存储取消节点 Node 的后驱节点
Node next = node.next;
// next 节点必须是不被取消的哦
if (next != null && next.waitStatus <= 0)
// 然后把 pred 的后驱指针指向 next 即可,这样就维护了这个双向链表的引用。
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
中断很多情况下都不会结束线程运行,所以这里再看一种不被暴力结束线程运行的情况,代码如下:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
假设有人中断了这个阻塞挂起的线程,那么Thread.interrupted();
会返回 true
,并且会复位重新把线程中断标识修改成 false
,然后再看到外层代码,如下:
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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
直接把 interrupted
赋值为 true
,假设它被唤醒过后又尝试加锁,而且加锁还成功了,那么这里 return
出去的是 true
,再看到外层代码,如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
执行 selfInterrupt()
方法,再次把线程中断标识修改成了true
,因为线程中断标识是你修改的,后来被 Thread.interrputed()
程序自动复位了,为了还原你自己的设置,所以这里又把线程中断标识位改为true
。这里只是针对你修改了中断标识位不做任何异常处理的1种情况,不会响应你的操作,如果你需要它响应你中断修改,那可以使用可中断式加锁去尝试加锁,对比图如下:
会发现可中断式的加锁只是多了中断标识位的判断,然后如果修改了中断标识位,就直接给你抛出异常,中断这个线程,然后发现此时会进入到 finally
兜底逻辑,执行 cancelAcquire()
方法,这里需要注意下,其他逻辑都是一样的。
前面这两种都是前驱节点取消了(状态异常)去维护 CHL 队列的。
cancelAcquire()
shouldParkAfterFailedAcquire()
那么现在如果某个节点正在运行区间 Running,然后它去驱动启动后驱线程时,发现他的后驱节点被强制中断了,状态变成了取消状态,那么后驱节点被取消,又在哪里去维护 CHL 队列呢?可以看到释放锁的地方,代码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
进入 unparkSuccessor()
方法内部,代码如下:
private void unparkSuccessor(Node node) {
// 检查节点的 waitStatus 是否为 -1,因为 -1 等待被唤醒的状态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 出现异常的情况就在这里维护 CLH 队列
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);
}
Node s = node.next;
取出该节点的后驱节点,s == null
可能存在两种情况,第一种就是 node 是尾节点,自然而然后驱指针肯定是null
的,另一种因为线程是并发操作的,还没来得及给 node 的后驱节点赋值,所以也可能是null
。然后开始执行下面这几行代码:
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
先展示 CLH 队列中有两个取消的 Node 节点,如下图示:
- 首先 t 指向尾节点,也就是上图中的 Node6 节点
- 然后判断 t != node 成立,因为 t=Node6,node 现在等于当前正在运行的 Node1 节点
- t.prev 表示获取 Node6 的前驱节点为 Node5,此时 t=Node5,waitStatus = 0 成立,赋值给 s ,此时 s 也指向 Node5
- 继续 for,此时 t = Node5,执行 t.prev 之后,t = Node4,发现 waitStatus=1 不成立,s 此时还是指向 Node5
- 继续 for,此时 t = Node4,执行 t.prev 之后,t = Node3,发现 waitStatus=0 成立,s 指向 Node3
- 继续 for,此时 t = Node3,执行 t.prev 之后,t = Node2,发现 waitStauts=1 不成立,s 还是指向 Node3
- 继续 for,此时 t = Node2,执行 t.prev 之后,t = Node1,然后判断 t!=node 不成立,跳出循环,此时 s 指向了 Node3
- 此时的 s 就是当前运行节点的后驱节点,也就是说 Node3 将会作为 Node1 节点的后驱节点,Node1 运行完就要去唤醒 Node3,至此 Node2 就被踢出了 CHL 队列,这样就维护了在运行期间,有后驱节点被中断取消的情况维护了 CHL 队列的正常秩序。
可以发现这是从尾部往前去寻找第一个不被取消的节点,然后当前线程运行完,就回去去唤醒这个找寻到的线程。
超时获取可中断锁
直接看代码,如下所示:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
可以发现其他和可中断锁差不多,判断了线程标识位是否被修改过,如果有修改过就响应中断请求,直接给你抛出异常程序结束。
然后在进入到方法 doAcquireNanos()
内部,代码如下:
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果设置等待超时时间为负数,直接返回false,加锁失败。
if (nanosTimeout <= 0L)
return false;
// 当前系统时间 + 等待超时时间
final long deadline = System.nanoTime() + nanosTimeout;
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 true;
}
// 计算超时时间还够不够
// 当从第一次挂起之后有被唤醒过来再次执行,肯定是超时的了,直接回走 false,加锁失败。
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false; // 不够直接返回 false,表示在等待时间内还没有加锁成功
// 如果你设定超时时间比系统给你设定的 1000L ns 还要多,那不好意思,直接用系统设定的时间
// 不可能一直用你的超时时间去自旋,耗费 cpu 资源,所以超过了就直接挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 如果有中断就直接响应中断请求,直接抛出异常,现成结束运行,但是 finally 逻辑还是会执行的
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
// 响应中断请求,有人修改过了线程中断标识
if (failed)
cancelAcquire(node);
}
}
释放锁都是同一个,因为不管你是可中断,还是超时中断锁,都是独占锁模式下,也就一次性只能有一个人获取到锁,释放都是把这个标识位复位而已,所以都操作同一个 release(1L)
方法即可。
方法之间的调用关系
推荐阅读文章
1、使用 Spring 框架构建 MVC 应用程序:初学者教程
2、有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
3、如何理解应用 Java 多线程与并发编程?
4、Java Spring 中常用的 @PostConstruct 注解使用总结
5、线程 vs 虚拟线程:深入理解及区别
6、深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
7、10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
8、“打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”
9、Java 中消除 If-else 技巧总结
10、线程池的核心参数配置(仅供参考)
11、【人工智能】聊聊Transformer,深度学习的一股清流(13)
12、Java 枚举的几个常用技巧,你可以试着用用
13、如何理解线程安全这个概念?
14、理解 Java 桥接方法
15、Spring 整合嵌入式 Tomcat 容器
16、Tomcat 如何加载 SpringMVC 组件