[Java并发] AQS抽象队列同步器源码解析--锁获取过程
[Java并发] AQS抽象队列同步器源码解析--独占锁释放过程
要深入了解java并发知识,AbstractQueuedSynchronizer(AQS)是必须要拿出来深入学习的,AQS可以说是贯穿了整个JUC并发包,例如ReentrantLock,CountDownLatch,CyclicBarrier等并发类都涉及到了AQS。接下来就对AQS的实现原理进行分析。
在开始分析之前,势必先将CLH同步队列了解一下
CLH同步队列
CLH自旋锁: CLH(Craig, Landin, and Hagersten locks): 是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。CLH自旋锁是一种基于隐式链表(节点里面没有next指针)的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
AQS中的CLH同步队列:AQS中CLH同步队列是对CLH自旋锁进行了优化,其主要从两方面进行了改造:节点的结构与节点等待机制。
1.在结构上引入了头结点和尾节点,他们分别指向队列的头和尾,尝试获取锁、入队列、释放锁等实现都与头尾节点相关,并且每个节点都引入前驱节点和后后续节点的引用;
2.在等待机制上由原来的自旋改成阻塞唤醒。
源码中CLH的简单表示
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
Node就是实现CLH同步队列的数据结构,计算下就了解下该类的相关字段属性
AQS中重要的内部类Node
static final class Node {
// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
// 如果属性waitStatus == Node.CANCELLED,则表明该节点已经被取消
static final int CANCELLED = 1;
// 如果属性waitStatus == Node.SIGNAL,则表明后继节点等待被唤醒
static final int SIGNAL = -1;
// 如果属性waitStatus == Node.CONDITION,则表明是Condition模式中的节点等待条件唤醒
static final int CONDITION = -2;
// 如果属性waitStatus == Node.PROPAGATE,在共享模式下,传播式唤醒后继节点
static final int PROPAGATE = -3;
// 用于标记当前节点的状态,取值为1,-1,-2,-3,分别对应以上4个取值
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 当前节点对应的线程,阻塞与唤醒的线程
volatile Thread thread;
// 使用Condtion时(共享模式下)的后继节点,在独占模式中不会使用
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
下面就开始着重对AQS中的重要方法进行分析说明
获取锁
1.acquire 开始获取锁
public final void acquire(int arg) {
//如果tryAcquire返回true,即获取到锁就停止执行,否则继续向下执行向同步队列尾部添加节点
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire是用于获取锁的方法,在AQS中默认没有实现具体逻辑,由子类自定义实现。
如果返回true则说明获取到锁,否则需要将当前线程封装为Node节点添加到同步队列尾部。
2.当前节点入队列
将当前执行的线程封装为Node节点并加入到队尾
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)添加到队尾
enq(node);
return node;
}
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;
}
}
}
}
addWaiter(Node mode)方法执行完后,接下来执行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);
}
}
shouldParkAfterFailedAcquire(p, node)方法判断是否需要阻塞当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 如果ws == Node.SIGNAL,则说明当前线程已经准备好被唤醒,因此现在可以被阻塞,之后等待被唤醒
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// 如果ws > 0,说明当前节点已经被取消,因此循环剔除ws>0的前驱节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果ws<=0,则将标志位设置为Node.SIGNAL,当还不可被阻塞,需要的等待下次执行shouldParkAfterFailedAcquire判断是否需要阻塞
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果shouldParkAfterFailedAcquire(p, node)方法返回true,说明需要阻塞当前线程,则执行parkAndCheckInterrupt方法阻塞线程,并返回阻塞过程中线程是否被中断
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞线程,等待unpark()或interrupt()唤醒自己
// 线程被唤醒后查看是否被中断过。
return Thread.interrupted();
}
那么重新回到获取锁的方法acquire方法,如果acquireQueued(final Node node, int arg)返回true,也即是阻塞过程中线程被中断,则执行中断线程操作selfInterrupt()
public final void acquire(int arg) {
//如果tryAcquire返回true,即获取到锁就停止执行,否则继续向下执行向同步队列尾部添加节点,然后判断是否被中断过,是则执行中断
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
中断当前线程
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
小结
AQS获取锁的过程:
1.执行tryAcquire方法获取锁,如果获取锁成功则直接返回,否则执行步骤2
2.执行addWaiter方法将当前线程封装位Node节点并添加到同步队列尾部,执行步骤3
3.执行acquireQueued循环尝试获取锁,,如果获取锁成功,则判断返回中断标志位,如果获取锁失败则调用shouldParkAfterFailedAcquire方法判断是否需要阻塞当前线程,如果需要阻塞线程则调用parkAndCheckInterrupt阻塞线程,并在被唤醒后再判断再阻塞过程中是否被中断过。
4.如果acquireQueued返回true,说明在阻塞过程中线程被中断过,则执行selfInterrupt中断线程
好了,以上就是AQS的锁获取过程,关于锁释放的分析会在后续继续输出。
[Java并发] AQS抽象队列同步器源码解析--锁获取过程的更多相关文章
- [Java并发] AQS抽象队列同步器源码解析--独占锁释放过程
[Java并发] AQS抽象队列同步器源码解析--独占锁获取过程 上一篇已经讲解了AQS独占锁的获取过程,接下来就是对AQS独占锁的释放过程进行详细的分析说明,废话不多说,直接进入正文... 锁释放入 ...
- Java并发编程之CAS二源码追根溯源
Java并发编程之CAS二源码追根溯源 在上一篇文章中,我们知道了什么是CAS以及CAS的执行流程,在本篇文章中,我们将跟着源码一步一步的查看CAS最底层实现原理. 本篇是<凯哥(凯哥Java: ...
- 深入理解Java AIO(二)—— AIO源码解析
深入理解Java AIO(二)—— AIO源码解析 这篇只是个占位符,占个位置,之后再详细写(这个之后可能是永远) 所以这里只简单说一下我看了个大概的实现原理,具体的等我之后更新(可能不会更新了) 当 ...
- Java并发包源码学习系列:基于CAS非阻塞并发队列ConcurrentLinkedQueue源码解析
目录 非阻塞并发队列ConcurrentLinkedQueue概述 结构组成 基本不变式 head的不变式与可变式 tail的不变式与可变式 offer操作 源码解析 图解offer操作 JDK1.6 ...
- Java并发编程笔记之AbstractQueuedSynchronizer源码分析
为什么要说AbstractQueuedSynchronizer呢? 因为AbstractQueuedSynchronizer是JUC并发包中锁的底层支持,AbstractQueuedSynchroni ...
- AbstractQueuedSynchronizer 队列同步器源码分析
AbstractQueuedSynchronizer 队列同步器(AQS) 队列同步器 (AQS), 是用来构建锁或其他同步组件的基础框架,它通过使用 int 变量表示同步状态,通过内置的 FIFO ...
- Java 集合系列Stack详细介绍(源码解析)和使用示例
Stack简介 Stack是栈.它的特性是:先进后出(FILO, First In Last Out). java工具包中的Stack是继承于Vector(矢量队列)的,由于Vector是通过数组实现 ...
- Java并发包下锁学习第二篇Java并发基础框架-队列同步器介绍
Java并发包下锁学习第二篇队列同步器 还记得在第一篇文章中,讲到的locks包下的类结果图吗?如下图: 从图中,我们可以看到AbstractQueuedSynchronizer这个类很重要(在本 ...
- 学习JUC源码(1)——AQS同步队列(源码分析结合图文理解)
前言 最近结合书籍<Java并发编程艺术>一直在看AQS的源码,发现AQS核心就是:利用内置的FIFO双向队列结构来实现线程排队获取int变量的同步状态,以此奠定了很多并发包中大部分实现基 ...
随机推荐
- [LC]530题 二叉搜索树的最小绝对差
①题目 给定一个所有节点为非负值的二叉搜索树,求树中任意两节点的差的绝对值的最小值. 示例 : 输入: 1 \ 3 / 2 输出:1 解释:最小绝对差为1,其中 2 和 1 的差的绝对值为 ...
- spark-Worker内部工作流程
- requests模块发送带headers的Get请求和带参数的请求
1.在PyCharm开发工具中新建try_params.py文件: 2.try_params.py文件中编写代码: import requests#设置请求Headers头部header = {&qu ...
- SpringBoot 源码解析 (七)----- Spring Boot的核心能力 - 自定义Servlet、Filter、Listener是如何注册到Tomcat容器中的?(SpringBoot实现SpringMvc的原理)
上一篇我们讲了SpringBoot中Tomcat的启动过程,本篇我们接着讲在SpringBoot中如何向Tomcat中添加Servlet.Filter.Listener 自定义Servlet.Filt ...
- java基础开发环境安装(全)
一.jdk安装(可以根据自己习惯选择合适安装路径) 1.jdk1.8下载地址:https://pan.baidu.com/s/1O9JQlFJ9cpkGCQL35cm_7g 提取码:pe2g 2.jd ...
- x86汇编分页模式实验 --《ORANGE'S一个操作系统的实现》中 pmtest8.asm解析
序言(废话) : 在看书的过程中发现一开始不是很能理解pmtest8的目的,以及书上说得很抽象..于是在自己阅读过源代码后,将一些自己的心得写在这里. 正文 : 讲解顺序依然按照书上贴代码的顺序来.但 ...
- 白话布隆过滤器BloomFilter
通过本文将了解到以下内容: 查找问题的一般思路 布隆过滤器的基本原理 布隆过滤器的典型应用 布隆过滤器的工程实现 场景说明: 本文阐述的场景均为普通单机服务器.并非分布式大数据平台,因为在大数据平台下 ...
- selenium针对浏览器滚动条的操作
我们在实际自动化测试过程中,肯定会遇到当前页面显示不到我们定位的元素.这就需要下拉滚动条才能显示出我们的元素: 而滚动条的按钮又是我们定位不到的,所以需要使用js脚本来完成: 1.先来说我们的下拉滚动 ...
- 【Stream—6】BufferedStream相关知识分享
一.简单介绍以下BufferedStream 在前几章的讲述中,我们已经能够掌握流的基本特性和特点,一般进行对流的处理时,系统肩负着IO所带来的开销,调用十分频繁,这时候就应该想个办法减少这种开销,而 ...
- 20191010-9 alpha week 1/2 Scrum立会报告+燃尽图 07
此作业要求参见https://edu.cnblogs.com/campus/nenu/2019fall/homework/8752 一.小组情况 队名:扛把子 组长:迟俊文 组员:宋晓丽 梁梦瑶 韩昊 ...