AQS 源码解读之加锁篇
以 ReentrantLock 创建的非公平锁为基础,进行 AQS 全流程的分析。
分析 demo
一共有 A、B、C 三个线程。
public class AQSDemo {
// 带入一个银行办理业务的案例
public static void main(String[] args) {
// 创建一个非公平锁
ReentrantLock lock = new ReentrantLock();
// 三个线程模拟3个网点
// A 顾客就是第一个顾客,此时没有没有其他顾客
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t thread come in");
try {
TimeUnit.MINUTES.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
// 第二个线程 --> 由于受理窗口只有一个(只能一个线程持有锁),此时 B 只能等待
// 进入候客区
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t thread come in");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "B").start();
// 第三个线程 --> 由于受理窗口只有一个(只能一个线程持有锁),此时 B 只能等待
// 进入候客区
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t thread come in");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "C").start();
}
}
线程 A
lock 方法分析
第一步:
调用抽象类 sync 的抽象 lock() 方法
public void lock() {
sync.lock();
}
第二步:
抽象类 sync 的具体实现
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
执行 compareAndSetState(0, 1) 方法
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
这个方法就是将 state
值进行比较修改,由于这个是第一个线程进来,所以通过比较修改,将 state
的值从默认的 0 改成了 1,然后返回 true。
执行 setExclusiveOwnerThread(Thread.currentThread()) 方法
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
设置当前拥有独占访问权限的线程,对应 Demo 中的 A 线程。
总结
第一个线程的执行逻辑比较简单,直接修改 state 和将当前占有的线程改成自己就可以了。
线程 B
lock 方法分析
第一步:
和第一个线程执行的是一样的代码。
第二步:
2.1 抽象类 sync 的具体实现
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
2.2 执行 compareAndSetState(0, 1) 方法
// expect = 0 update = 1
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
此时当线程二再次执行比较并修改方法,想修改 state 的值时,通过比较对比。此时 state 的值已经被线程一修改成了 1,所以此时修改失败。返回 false。
2.3 执行 acquire(1) 方法 1197
// arg = 1
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2.4 执行 !tryAcquire(arg) 方法
2.4.1 此方法是 AQS 中的抽象类,需要查看器具体实现
通过抛出异常的方式,强制子类必需实现其钩子程序。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
2.4.2 找到具体实现,在 NonfairSync 类中,上面的代码可以 [点击查看](##### 2.1 抽象类 sync 的具体实现)。
// acquires = 1
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
2.5 执行了 nonfairTryAcquire(acquires) 方法
// acquires = 1
final boolean nonfairTryAcquire(int acquires) {
// 此时 Thread = 第二个线程
final Thread current = Thread.currentThread();
// getState() 方法返回 1,因为 state 已经被第一个线程所修改了
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// getExclusiveOwnerThread:获取当前占用锁的线程,也就是线程一
// 此时如果线程一再次过来获取到锁,就可以直接进去也就是可重入锁
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;
}
nonfairTryAcquire 方法首先校验了 state 的值是否等于 0,也就是看看上一个占用锁的线程是不是已经把资源给释放了。
后续又校验了当前线程是不是和占用锁的线程是同一个,也就是一个可重入锁。
线程 B 来判断的话都不满足条件,所以返回 false。
返回 false 后 [!tryAcquire(arg) = true](######2.3 执行 acquire(1) 方法),所以继续执行。
2.6 执行 addWaiter(Node.EXCLUSIVE) 方法
Node.EXCLUSIVE:static final Node EXCLUSIVE = null; 也就是排他的意思。
// mode = null
private Node addWaiter(Node mode) {
// 创建一个 Node 节点
Node node = new Node(Thread.currentThread(), mode);
// tail:尾节点,此时为 null
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
2.6.1 new Node(Thread.currentThread(), mode);
// thread = 线程二 mode = null
Node(Thread thread, Node mode) {
// 将当前等待节点谁知为 null
this.nextWaiter = mode;
// 这个 Node 节点的线程设置为 线程二
this.thread = thread;
}
2.6.2 enq(node)
// node 等于 2.6.1 创建的 Node 节点
private Node enq(final Node node) {
// 自旋操作
for (;;) {
// 此时尾结点 tail 为 null
Node t = tail;
// t is null 进行初始化操作,这一步也就是意味着此时 CLH 队列中还没有任何一个元素。
if (t == null) {
// 成功将队列里面的头节点替换成新创建的 Node 节点
if (compareAndSetHead(new Node()))
// 将尾结点也指向新创建 Node 节点
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
2.6.3 compareAndSetHead(new Node())
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
比较并替换掉头节点,如果是 null 的话,直接将头节点替换成新创建的 Node 节点。
这里需要注意的是:队列中的第一个节点并不是我们线程二这个节点,而是系统自动帮我们创建了一个新的 Node 节点。
替换成功返回 true 继续执行 if 语句里面的[代码](######2.6.2 enq(node));
经过第一轮循环,此时 CLH 中的情况:
2.6.4 enq(node) 第二次循环
// node = 2.6.1 创建的 Node 节点,也就是线程二的 Node 节点
private Node enq(final Node node) {
// 自旋操作
for (;;) {
// 此时尾结点 tail 为 新创建的 node 节点
Node t = tail;
// t is not null 执行 else
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 将 node 节点的前指针指向新创建的头节点
node.prev = t;
// 通过比较替换将队列的尾结点替换了线程 B 的 Node 节点
if (compareAndSetTail(t, node)) {
// 将系统初始的 Node 的后指针指向线程 B 的 Node 节点
t.next = node;
// 然后返回 B 线程的 Node 节点
return t;
}
}
}
}
经过第二次循环后,此时的 CLH 队列的情况如下
2.7 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
// node:线程 B 对应的 Node 节点 arg = 1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋操作
for (;;) {
// 获取现在队列中的第一个节点也就是系统创建的 Node 节点
final Node p = node.predecessor();
// P 现在是头节点,true。但是线程 B 尝试获取锁失败,false
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 所以执行这一步,返回 false。所以进行下一次循环
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2.7.1 node.predecessor();
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
2.7.2 tryAcquire(arg)
再走一遍 2.5 执行了 [!nonfairTryAcquire(acquires) 方法](#####2.5 执行了 nonfairTryAcquire(acquires) 方法)
2.7.3 shouldParkAfterFailedAcquire(p, node)
第一次循环
// pred 系统创建的 Node 节点,node 线程 B 对应的 Node 节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 此时头节点的 waitStatus = 0
int ws = pred.waitStatus;
// Node.SIGNAL = -1
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 通过比较替换,将头节点的值从 0 调整为 -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
现在对应 CLH 队列中的情况:
该方法将头节点中 Node 的 waitStatus 的值改成了 -1,并且返回了 false。
然后再次重复 [2.7 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)](######2.7 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 的操作,
2.7.4 shouldParkAfterFailedAcquire(p, node)
第二次循环
// node:线程 B 对应的 Node 节点 arg = 1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋操作
for (;;) {
// 获取现在队列中的第一个节点也就是系统创建的 Node 节点
final Node p = node.predecessor();
// P 现在是头节点,true。但是线程 B 尝试获取锁失败,false
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 所以第二次循环后返回 ture,执行下一步
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// pred 系统创建的 Node 节点,node 线程 B 对应的 Node 节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 经过第一次循环操作,此时头节点的 waitStatus = -1
int ws = pred.waitStatus;
// Node.SIGNAL = -1
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 通过比较替换,将头节点的值从 0 调整为 -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
2.7.5 parkAndCheckInterrupt()
// this 当前 B 线程
private final boolean parkAndCheckInterrupt() {
// 将当前线程 B 进行挂起
LockSupport.park(this);
return Thread.interrupted();
}
此时,线程B相当于已经完成了入队操作,进行了挂起。不会再尝试去获取锁了,安安心心在 CLH 队列中等待唤醒操作。
获取锁小总结
线程 B 在锁已经被占用的情况下,会先去尝试抢占锁。如果抢占失败,AQS 回将线程 B 进行入队操作。但是在入队之前会先进行初始化操作,也就是先创建一个傀儡节点,由其充当头节点和尾结节点。
队列初始化完成后,再将线程 B 对应的 Node 节点与傀儡节点进行连接,也就是傀儡节点的尾指针指向线程 B 的 Node 节点,线程 B 的 Node 节点的指针指向傀儡节点。最后将 CLH 队列的尾指针指向线程 B 的 Node 节点。
将acquire线程 B 的 Node 节点加入到 CLH 队列中后又调用了 acquireQueued 方法,这里通过自旋使得线程 B 又抢占了两次锁,抢占到了的就进行后面的操作,没有抢占到便执行 parkAndCheckInterrupt 方法,将自己挂起,等待前面的线程执行完释放锁后将自己唤醒。
线程 C
lock 方法分析
线程 C 和线程 B 前面执行的逻辑是一样,直到执行 addWaiter(Node.EXCLUSIVE) 方法时才有所出入,所以就直接分析 addWaiter(Node.EXCLUSIVE) 方法的执行流程。
addWaiter(Node.EXCLUSIVE)
// mode 还是等于 null 排他
private Node addWaiter(Node mode) {
// 创建线程 C 的 Node 节点
Node node = new Node(Thread.currentThread(), mode);
// 此时尾指针指向的是线程 B 的 Node 节点
Node pred = tail;
if (pred != null) {
// 将线程 C 的 Node 节点的前指针指向线程 B 的 Node 节点
node.prev = pred;
// 通过比较替换将线程的尾指针指向线程 C 的 Node 节点
if (compareAndSetTail(pred, node)) {
// 将线程 B 的 Node 节点的后指针指向线程 C 的 Node 节点
pred.next = node;
// 返回线程 C 的 Node 节点
return node;
}
}
enq(node);
return node;
}
线程 C 执行完 addWaiter 方法后此时的 CLH 队列的情况如下:
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
// node 线程 c 对应的 Node arg = 1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取线程 c 对应的 Node 节点的前指针对应的 Node(线程 B 的 Node 节点线程 B 的 Node 节点)
final Node p = node.predecessor();
// 因为此时 head 头节点还是傀儡节点,所以不匹配,直接执行下面的代码
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);
}
}
shouldParkAfterFailedAcquire(p, node)
第一次执行
// pred 线程 B 对应的 Node node 线程 c 对应的 Node
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 此时线程 B 的 waitStatus = 0
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 {
// 通过比较替换将线程 B 的 waitStatus 改成 -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
第二次执行
// pred 线程 B 对应的 Node node 线程 c 对应的 Node
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 此时线程 B 的 waitStatus = -1
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;
}
parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
线程 C 执行 parkAndCheckInterrupt 方法将自己挂起。
此时 CHL 队列的情况如下:
总结
线程 C 执行的流程和线程 B 大致是差不太多的,但是线程 C 和线程与线程 B 最显著的区别就是少了两次锁的抢占
。在方法 acquireQueued 中,由于线程 C 的前指针指向的 Node 节点与头节点的 Node 不一样,所以就直接跳过了,不会执行后续的尝试抢占锁的方法。
后面线程 C 执行 shouldParkAfterFailedAcquire 方法将其前指针指向的 Node 节点中的 waitStatus 的值从 0 改成了 -1。最后就是执行 parkAndCheckInterrupt 方法,将自己挂起。
AQS 源码解读之加锁篇的更多相关文章
- spring beans源码解读之--总结篇
spring beans下面有如下源文件包: org.springframework.beans, 包含了操作java bean的接口和类.org.springframework.beans.anno ...
- AQS源码解读(ReentrankLock的公平锁和非公平锁)
构建Debug代码: 1 package com.hl.interview.lock; 2 3 import java.util.Scanner; 4 import java.util.concurr ...
- Bootstrap源码解读之栅格化篇
本文纯属自己研究所写笔记,如果有错误还请多多指教提出 版心(container) 版心:class名为.container的容器,其版心的宽度在各个屏幕设备下是不一样的值,版心两边就是留白. 各尺寸下 ...
- 硬核剖析Java锁底层AQS源码,深入理解底层架构设计
我们常见的并发锁ReentrantLock.CountDownLatch.Semaphore.CyclicBarrier都是基于AQS实现的,所以说不懂AQS实现原理的,就不能说了解Java锁. 上篇 ...
- SDWebImage源码解读 之 NSData+ImageContentType
第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...
- AQS源码二探-JUC系列
本文已在公众号上发布,感谢关注,期待和你交流. AQS源码二探-JUC系列 共享模式 doAcquireShared 这个方法是共享模式下获取资源失败,执行入队和等待操作,等待的线程在被唤醒后也在这个 ...
- AQS源码详细解读
AQS源码详细解读 目录 AQS源码详细解读 基础 CAS相关知识 通过标识位进行线程挂起的并发编程范式 MPSC队列的实现技巧 代码讲解 独占模式 独占模式下请求资源 独占模式下的释放资源 共享模式 ...
- [Hadoop源码解读](六)MapReduce篇之MapTask类
MapTask类继承于Task类,它最主要的方法就是run(),用来执行这个Map任务. run()首先设置一个TaskReporter并启动,然后调用JobConf的getUseNewAPI()判断 ...
- Laravel 源码解读系列第四篇-Auth 机制
前言 Laravel有一个神器: php artisan make:auth 能够快速的帮我们完成一套注册和登录的认证机制,但是这套机制具体的是怎么跑起来的呢?我们不妨来一起看看他的源码.不过在这篇文 ...
随机推荐
- CSS 3D的魅力
用户1093975发表于Web项目聚集地订阅 151 在这篇文章中: 前言: demo1 demo2 结语: 本文介绍了CSS来实现3D效果,并且有详细代码和解释.建议大家只字不差的阅读.本文的作者是 ...
- iOS App程序内多语言国际化实现 By HL
iOS 多语言设置有很多方式可以实现,之前在做手机思埠1.0时,就对app进行了多语言设置,当时看到很多方法,比如用plist等方式保存键值对的,不过还是用Localisator来国际化最方便 1.添 ...
- 2022寒假集训day5
day5 五道栈的题加上字符串. 单调队列. T1 表达式括号匹配 洛谷P1739 题目描述 假设一个表达式有英文字母(小写).运算符(+,-,*,/)和左右小(圆)括号构成,以"@&q ...
- 问题描述:Navicat连不上MySQL数据库
发现Navicat连不上MySQL后我首先觉得是MySQL的服务没有打开, 然后再cmd里面 启动MySQL服务 输入: net start mysql 发现没有此服务,然我网上找了一下 先初始化My ...
- 记录netcore一次内存暴涨的坑
项目用到了Coldairarrow/EFCore.Sharding: Database Sharding For EFCore (github.com)这个组件,最初是因为分表做的还不错所以用了它. ...
- Ceres 四重奏 之 入门简介
Ceres 翻译为谷神星,是太阳系中的一颗矮行星,于1801年被意大利神父 Piazzi 首次观测到,但随后 Piazzi 因为生病,跟丢了它的运行轨迹. 几个月后,德国数学家 Gauss,利用最小二 ...
- pytest(10)-常用执行参数说明
pytest单元测试框架中可以使用命令行及代码pytest.main()两种方式执行测试,且可以加入各种参数来组织执行测试.接下来我们来了解常用的执行参数的含义及其用法. pytest中的执行参数根据 ...
- kubernetes之数据管理
volume emptyDir [machangwei@mcwk8s-master ~]$ kubectl apply -f mcwVolume1.yml #部署emptydir pod/produc ...
- 使用JS简单实现一下apply、call和bind方法
使用JS简单实现一下apply.call和bind方法 1.方法介绍 apply.call和bind都是系统提供给我们的内置方法,每个函数都可以使用这三种方法,是因为apply.call和bind都实 ...
- can_has_stdio?
得到一个用±<>这样符号组成的五角星,结合题目stdio,估计是c语言编译后的文件 查到BrianFuck语言,找个在线编译器或者找到编译码(c++)得到flag 在线编译网站 brain ...