从ReentrantLock详解AQS原理源码解析
数据结构
java.util.concurrent.locks.AbstractQueuedSynchronizer类中存在如下数据结构。
// 链表结点
static final class Node {}
// head指向的是一个虚拟结点,刷多了算法就知道这样做的目的是方便对链表操作,真正的头为head.next
private transient volatile Node head;
// 尾结点
private transient volatile Node tail;
// 同步状态,用于展示当前临界资源的获锁情况。
private volatile int state;
// 继承至AbstractOwnableSynchronizer类
// 独占模式下当前锁的拥有者
private transient Thread exclusiveOwnerThread;
// 自旋锁的自旋纳秒数,用于提高应用的响应能力
static final long spinForTimeoutThreshold = 1000L;
// unsafe类
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 以下字段对应上面字段的在对象中的偏移值,在静态代码块中初始化,其值是相对于在这个类对象中的偏移量
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
在AQS类中内部类Node包含如下数据结构
static final class Node {
// 共享锁
static final Node SHARED = new Node();
// 独占锁
static final Node EXCLUSIVE = null;
// 0 当一个Node被初始化的时候的默认值
// CANCELLED 为 1,表示线程获取锁的请求已经取消了
// CONDITION 为 -2,表示节点在等待队列中,节点线程等待唤醒
// PROPAGATE 为 -3,当前线程处在SHARED情况下,该字段才会使用
// SIGNAL 为 -1,表示线程已经准备好了,就等资源释放了
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 前驱指针
volatile Node prev;
// 后继指针
volatile Node next;
// 该节点代表的线程对象
volatile Thread thread;
Node nextWaiter;
}
从其数据结构可以猜测出
- AQS类中主要的存储结构应该是一个双向链表。
- state字段对应了这个锁对象的状态。
- 线程申请锁时会将其包装成一个节点。Node保存了获取锁的线程信息。
- Node.waitStatus字段保存这个线程申请锁的状态。
- head指向的是一个虚拟结点,真正有效的头为head.next
源码分析
我们从AQS的实现类ReentrantLock#lock开始分析其具体的流程。
ReentrantLock#lock
public void lock() {
sync.lock();
}
直接调用了Sync类的lock()方法,Sync类在ReentrantLock中有两个实现类分别是FairSync和NonfairSync,分别对应了公平锁和非公平锁。
- 公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;
- 非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。
由于ReentrantLock默认是非公平锁,我们从NonfairSync类分析。
ReentrantLock.NonfairSync#lock
final void lock() {
// cas操作尝试将state字段值修改为1
if (compareAndSetState(0, 1))
// 成功的话就代表已经获取到锁,修改独占模式下当前锁的拥有者为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 获取锁失败之后的操作
acquire(1);
}
从这可以确定我们之前的猜测
- state字段对应了这个锁对象的状态,值为0的时候代表锁没有被线程占用,修改为1之后代表锁被占用。
现在分析未获取到锁之后的流程
AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
if (
// 当前线程尝试获取锁
!tryAcquire(arg) &&
// acquireQueued会把传入的结点在队列中不断去获取锁,直到获取成功或者不再需要获取(中断)。
acquireQueued(
// 在双向链表的尾部创建一个结点,值为当前线程和传入的模式
addWaiter(Node.EXCLUSIVE),
arg
)
)
// TODO
selfInterrupt();
}
看不懂,先查找资料了解这几个方法的作用,注释在代码中。
ReentrantLock.NonfairSync#tryAcquire
// 当前线程尝试获取锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
ReentrantLock.Sync#nonfairTryAcquire
// 当前线程尝试获取锁-非公平
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获得当前锁对象的状态
int c = getState();
// state为0代表当前没有被线程占用
if (c == 0) {
// cas操作尝试将state字段值修改为请求的数量
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");
// state值增加相应的请求数。
setState(nextc);
return true;
}
return false;
}
ReentrantLock字面意思是可重入锁
- 可重入锁:一个线程在获取一个锁之后,在没有释放之前仍然可以继续申请锁而不会造成阻塞,但是解锁的时候也需要相应次数的解锁操作。
结合nonfairTryAcquire方法逻辑,可以推断出state字段在独占锁模式下还代表了锁的重入次数。
AbstractQueuedSynchronizer#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
// 快速路径,是为了方便JIT优化。jvm检测到热点代码,会将其编译成本地机器码并以各种手段进行代码优化。
Node pred = tail;
if (pred != null) {
// 将新创建的node的前驱指针指向tail。
node.prev = pred;
// 将结点修改为队列的tail时可能会发生数据冲突,用cas操作保证线程安全。
if (compareAndSetTail(pred, node)) {
// compareAndSetTail比较的地址,如果相等则将新的地址赋给该字段(而不是在源地址上替换,为什么我会这么想???)
// 所以此处pred引用指向的仍然是源tail的内存地址。将其后继指针指向新的tail
pred.next = node;
return node;
}
}
// 队列为空或者cas失败(说明被别的线程已经修改)
enq(node);
return node;
}
这个方法主要作用是在链表尾部创建一个结点,返回新创建的结点,其主要流程为
- 通过当前的线程和锁模式创建一个节点。
- 节点入尾操作
- 新节点的前驱指针指向tail
- 使用cas操作修改新节点为tail
- 原tail的后继指针指向新节点
当队列为空或者cas失败(说明被别的线程已经修改)会执行enq方法兜底。
AbstractQueuedSynchronizer#enq
// 在队列尾部创建一个结点,值为当前线程和传入的模式,当队列为空的时候初始化。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 创建一个空结点设置为头,真正的头为hdead.next
if (compareAndSetHead(new Node()))
// 尾等于头
tail = head;
} else {
// 这段逻辑跟addWaiter()中快速路径的逻辑一样。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter是对enq方法的一层封装,addWaiter首先尝试一个快速路径的在链表尾部创建一个结点,失败的时候回转入enq方法兜底,循环在链表尾部创建一个节点,直到成功为止。
这里有个疑问,为什么要在addWaiter方法中尝试一次在enq方法中能完成的在链表尾部创建一个节点的操作呢?其实是为了方便JIT优化。jvm检测到热点代码,会将其编译成本地机器码并以各种手段进行代码优化。了解更多1、了解更多2。
在链表尾插入需要
AbstractQueuedSynchronizer#acquireQueued
// acquireQueued会把传入的结点在队列中不断去获取锁,直到获取成功或者不再需要获取(中断)。
final boolean acquireQueued(final Node node, int arg) {
// 标记是否成功拿到锁
boolean failed = true;
try {
// 标记获取锁的过程中是否中断过
boolean interrupted = false;
// 开始自旋,要么获取锁,要么中断
for (;;) {
// 获得其前驱节点
final Node p = node.predecessor();
// 如果前驱节点为head代表现在节点node在队列有效数据的第一位,就尝试获取锁
if (p == head && tryAcquire(arg)) {
// 获取锁成功,把当前节点置为虚节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果存在以下情况就要判断当前node是否要被阻塞
// 1. p为头节点且获取锁失败 2. p不为头结点
if (shouldParkAfterFailedAcquire(p, node) &&
// 阻塞进程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 取消申请锁
cancelAcquire(node);
}
}
AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire
// 依赖前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 入参请求锁的node的前驱节点的状态
int ws = pred.waitStatus;
// 如果前驱节点的状态为"表示线程已经准备好了,就等资源释放了"
// 说明前驱节点处于激活状态,入参node节点需要被阻塞
if (ws == Node.SIGNAL)
return true;
// 只有CANCELLED状态对应大于0
if (ws > 0) {
do {
// 循环向前查找取消状态节点,把取消节点从队列中剔除
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 设置状态非取消的前驱节点等待状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
ReentrantLock#lock总结
到现在我们可以总结一下ReentrantLock#lock非公平锁方法的流程
未获取到锁的情况下函数调用流程
- ReentrantLock#lock
- ReentrantLock.Sync#lock
- ReentrantLock.NonfairSync#lock
- AbstractQueuedSynchronizer#acquire
- ReentrantLock.NonfairSync#tryAcquire
- ReentrantLock.Sync#nonfairTryAcquire
- AbstractQueuedSynchronizer#addWaiter
- AbstractQueuedSynchronizer#acquireQueued
描述
- 执行ReentrantLock的Lock方法。
- 会调用到内部类Sync的Lock方法,由于Sync#lock是抽象方法,根据ReentrantLock初始化选择的公平锁和非公平锁,执行相关内部类的Lock方法,cas修改state值获取锁,失败执行父类的Acquire方法。
- 父类的Acquire方法会执行子类实现的tryAcquire方法,因为tryAcquire需要自定义同步器实现,因此执行了ReentrantLock中的tryAcquire方法,由于ReentrantLock是通过公平锁和非公平锁内部类实现的tryAcquire方法,因此会根据锁类型不同,执行不同的tryAcquire。
- tryAcquire是获取锁逻辑,获取失败后,会执行框架AQS的后续逻辑,跟ReentrantLock自定义同步器无关。
// 公平锁加锁时判断等待队列中是否存在有效节点的方法。
// 返回False,当前线程可以争取共享资源;
// 返回True,队列中存在有效节点,当前线程必须加入到等待队列中。
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// 头不等于尾代表队列中存在结点返回true
// 但是还有一种特例,就是如果现在正在执行enq方法进行队列初始化,tail = head;语句运行之后
// 此时h == t,返回false,但是队列中
return h != t &&
// 从这可以看出真正的头结点是head.next,即说明head是一个无实际数据的结点,为了方便链表操作
((s = h.next) == null
// 有效头结点与当前线程不同,返回true必须加入到等待队列
|| s.thread != Thread.currentThread());
}
即时编译器
Java程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器。
这里所说的热点代码主要包括两类
- 被多次调用的方法
- 被多次执行的循环体
对于这两种情况,编译的目标对象都是整个方法体,而不会是单独的循环体
未完待续
从ReentrantLock详解AQS原理源码解析的更多相关文章
- SpringBoot Profile使用详解及配置源码解析
在实践的过程中我们经常会遇到不同的环境需要不同配置文件的情况,如果每换一个环境重新修改配置文件或重新打包一次会比较麻烦,Spring Boot为此提供了Profile配置来解决此问题. Profile ...
- Spring Boot中@ConfigurationProperties注解实现原理源码解析
0. 开源项目推荐 Pepper Metrics是我与同事开发的一个开源工具(https://github.com/zrbcool/pepper-metrics),其通过收集jedis/mybatis ...
- java基础(十八)----- java动态代理原理源码解析
关于Java中的动态代理,我们首先需要了解的是一种常用的设计模式--代理模式,而对于代理,根据创建代理类的时间点,又可以分为静态代理和动态代理. 静态代理 1.静态代理 静态代理:由程序员创建或特定工 ...
- 【Spring实战】Spring注解配置工作原理源码解析
一.背景知识 在[Spring实战]Spring容器初始化完成后执行初始化数据方法一文中说要分析其实现原理,于是就从源码中寻找答案,看源码容易跑偏,因此应当有个主线,或者带着问题.目标去看,这样才能最 ...
- vue双向绑定原理源码解析
当我们学习angular或者vue的时候,其双向绑定为我们开发带来了诸多便捷,今天我们就来分析一下vue双向绑定的原理. 简易vue源码地址:https://github.com/maxlove123 ...
- 【转】【Spring实战】Spring注解配置工作原理源码解析
一.背景知识 在[Spring实战]Spring容器初始化完成后执行初始化数据方法一文中说要分析其实现原理,于是就从源码中寻找答案,看源码容易跑偏,因此应当有个主线,或者带着问题.目标去看,这样才能最 ...
- Spring注解Component原理源码解析
在实际开发中,我们经常使用Spring的@Component.@Service.@Repository以及 @Controller等注解来实现bean托管给Spring容器管理.Spring是怎么样实 ...
- 设计模式课程 设计模式精讲 8-8 单例设计模式-Enum枚举单例、原理源码解析以及反编译实战
1 课堂解析 2 代码演练 2.1 枚举类单例解决序列化破坏demo 2.2 枚举类单例解决序列化破坏原理 2.3 枚举类单例解决反射攻击demo 2.4 枚举类单例解决反射攻击原理 3 jad的使用 ...
- 【详解】ThreadPoolExecutor源码阅读(二)
系列目录 [详解]ThreadPoolExecutor源码阅读(一) [详解]ThreadPoolExecutor源码阅读(二) [详解]ThreadPoolExecutor源码阅读(三) AQS在W ...
随机推荐
- springboot手动事务回滚
亲测在使用@Transactional.@Transactional(rollbackFor = Exception.class)及catch异常之后 throw new RuntimeExcepti ...
- DJANGO-天天生鲜项目从0到1-001-环境框架搭建
本项目基于B站UP主‘神奇的老黄’的教学视频‘天天生鲜Django项目’,视频讲的非常好,推荐新手观看学习 https://www.bilibili.com/video/BV1vt41147K8?p= ...
- parted分区
((parted)mklabel ----创建磁盘标签New disk labeltype? gpt ---输入磁盘表情名(parted) p ----再次列出磁盘分区 (parted) mkpart ...
- 集合和Iterator迭代器
集合 集合是java中提供的一种容器,可以用来存储多个数据. 注意: ①.集合只能存放对象.比如你存一个 int 型数据 1放入集合中, 其实它是自动转换成 Integer 类后存入的,Java中每一 ...
- 01_Linux基础篇
学于黑马.传智播客.尚硅谷 感谢 黑马官网 传智播客官网 尚硅谷官网 微信搜索"艺术行者",关注并回复关键词"linux"获取视频和教程资料! b站在线视频 第 ...
- 图解HTTP 6/11
第三章 HTTP报文内的HTTP信息 1.用于http协议交互的信息被称为HTTP报文.请求端(客户端)的HTTP报文叫做请求报文,响应端(服务器端 )的叫做响应报文. 2.请求报文的结构 请求行:包 ...
- PHP fopen() 函数
定义和用法 fopen() 函数打开一个文件或 URL. 如果 fopen() 失败,它将返回 FALSE 并附带错误信息.您可以通过在函数名前面添加一个 '@' 来隐藏错误输出. 语法 fopen( ...
- PHP xml_parser_free() 函数
定义和用法 xml_parser_free() 函数释放 XML 解析器.高佣联盟 www.cgewang.com 如果成功,该函数则返回 TRUE.如果失败,则返回 FALSE. 语法 xml_pa ...
- 关于 ORA-01033: ORACLE initialization or shutdown in progress
第一步: 这个错误首先查看服务进程是否正常启动: 第二步: 一般情况下第一步都没问题,问题出在可能误删了日志文件: 当然可能不是你删除的,可能被某些清理软件删除的: 或者是其他情况导致日志出错 ...
- 使用Flask开发简单接口(4)--借助Redis实现token验证
前言 在之前我们已开发了几个接口,并且可以正常使用,那么今天我们将继续完善一下.我们注意到之前的接口,都是不需要进行任何验证就可以使用的,其实我们可以使用 token ,比如设置在修改或删除用户信息的 ...