learn.lianglianglee.com/文章/从ReentrantLock的实现看AQS的原理及应用.md.html
2022-05-11 19:04:14 +08:00

2571 lines
71 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>从ReentrantLock的实现看AQS的原理及应用.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/文章/AQS 万字图文全面解析.md.html">AQS 万字图文全面解析.md.html</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析.md.html</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通.md.html</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析.md.html</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器.md.html</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点.md.html</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结.md.html</a>
</li>
<li>
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析.md.html</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇).md.html</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中的SPI.md.html">Java中的SPI.md.html</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal.md.html</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践.md.html</a>
</li>
<li>
<a href="/文章/Java魔法类Unsafe应用解析.md.html">Java魔法类Unsafe应用解析.md.html</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记.md.html</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log).md.html</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享.md.html</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集.md.html</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL.md.html</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?.md.html</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC.md.html</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL优化优化 select count().md.html">MySQL优化优化 select count().md.html</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁.md.html</a>
</li>
<li>
<a href="/文章/MySQL的MVCC多版本并发控制.md.html">MySQL的MVCC多版本并发控制.md.html</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践.md.html</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦.md.html</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter.md.html</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理.md.html</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘.md.html</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖.md.html</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式.md.html</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition.md.html</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门.md.html</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践.md.html</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事.md.html</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战.md.html</a>
</li>
<li>
<a class="current-tab" href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用.md.html</a>
</li>
<li>
<a href="/文章/从SpringCloud开始聊微服务架构.md.html">从SpringCloud开始聊微服务架构.md.html</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft.md.html</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析.md.html</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计.md.html</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?.md.html</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践.md.html</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器.md.html</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?.md.html</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统.md.html</a>
</li>
<li>
<a href="/文章/异步网络模型.md.html">异步网络模型.md.html</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时我们在讨论些神马.md.html">当我们在讨论CQRS时我们在讨论些神马.md.html</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制.md.html</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答.md.html</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM).md.html</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践.md.html</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html">服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache.md.html</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现.md.html</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge.md.html</a>
</li>
<li>
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索.md.html</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?.md.html</a>
</li>
<li>
<a href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?.md.html</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践.md.html</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践.md.html</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现.md.html</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考.md.html</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题.md.html</a>
</li>
<li>
<a href="/文章/进阶Dockerfile 高阶使用指南及镜像优化.md.html">进阶Dockerfile 高阶使用指南及镜像优化.md.html</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构.md.html</a>
</li>
<li>
<a href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>从ReentrantLock的实现看AQS的原理及应用</h1>
<h2>前言</h2>
<p>Java中的大部分同步类Lock、Semaphore、ReentrantLock等都是基于AbstractQueuedSynchronizer简称为AQS实现的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。本文会从应用层逐渐深入到原理层并通过ReentrantLock的基本特性和ReentrantLock与AQS的关联来深入解读AQS相关独占锁的知识点同时采取问答的模式来帮助大家理解AQS。由于篇幅原因本篇文章主要阐述AQS中独占锁的逻辑和Sync Queue不讲述包含共享锁和Condition Queue的部分本篇文章核心为AQS原理剖析只是简单介绍了ReentrantLock感兴趣同学可以阅读一下ReentrantLock的源码</p>
<p>下面列出本篇文章的大纲和思路,以便于大家更好地理解:</p>
<p><img src="assets/9d182d944e0889c304ef529ba50a4fcd205214.png" alt="img" /></p>
<h2>1 ReentrantLock</h2>
<h3>1.1 ReentrantLock特性概览</h3>
<p>ReentrantLock意思为可重入锁指的是一个线程能够对一个临界资源重复加锁。为了帮助大家更好地理解ReentrantLock的特性我们先将ReentrantLock跟常用的Synchronized进行比较其特性如下蓝色部分为本篇文章主要剖析的点</p>
<p><img src="assets/412d294ff5535bbcddc0d979b2a339e6102264.png" alt="img" /></p>
<p>下面通过伪代码,进行更加直观的比较:</p>
<pre><code>// **************************Synchronized的使用方式**************************
// 1.用于代码块
synchronized (this) {}
// 2.用于对象
synchronized (object) {}
// 3.用于方法
public synchronized void test () {}
// 4.可重入
for (int i = 0; i &lt; 100; i++) {
synchronized (this) {}
}
// **************************ReentrantLock的使用方式**************************
public void test () throw Exception {
// 1.初始化选择公平锁、非公平锁
ReentrantLock lock = new ReentrantLock(true);
// 2.可用于代码块
lock.lock();
try {
try {
// 3.支持多种加锁方式,比较灵活; 具有可重入特性
if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
} finally {
// 4.手动释放锁
lock.unlock()
}
} finally {
lock.unlock();
}
}
</code></pre>
<h3>1.2 ReentrantLock与AQS的关联</h3>
<p>通过上文我们已经了解ReentrantLock支持公平锁和非公平锁关于公平锁和非公平锁的原理分析可参考《<a href="https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&amp;mid=2651749434&amp;idx=3&amp;sn=5ffa63ad47fe166f2f1a9f604ed10091&amp;chksm=bd12a5778a652c61509d9e718ab086ff27ad8768586ea9b38c3dcf9e017a8e49bcae3df9bcc8&amp;scene=38#wechat_redirect">不可不说的Java“锁”事</a>并且ReentrantLock的底层就是由AQS来实现的。那么ReentrantLock是如何通过公平锁和非公平锁与AQS关联起来呢 我们着重从这两者的加锁过程来理解一下它们与AQS之间的关系加锁过程中与AQS的关联比较明显解锁流程后续会介绍</p>
<p>非公平锁源码中的加锁流程如下:</p>
<pre><code>// java.util.concurrent.locks.ReentrantLock#NonfairSync
// 非公平锁
static final class NonfairSync extends Sync {
...
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
...
}
</code></pre>
<p>这块代码的含义为:</p>
<ul>
<li>若通过CAS设置变量State同步状态成功也就是获取锁成功则将当前线程设置为独占线程。</li>
<li>若通过CAS设置变量State同步状态失败也就是获取锁失败则进入Acquire方法进行后续处理。</li>
</ul>
<p>第一步很好理解,但第二步获取锁失败后,后续的处理策略是怎么样的呢?这块可能会有以下思考:</p>
<ul>
<li>某个线程获取锁失败的后续流程是什么呢?有以下两种可能:</li>
</ul>
<p>(1) 将当前线程获锁结果设置为失败获取锁流程结束。这种设计会极大降低系统的并发度并不满足我们实际的需求。所以就需要下面这种流程也就是AQS框架的处理流程。</p>
<p>(2) 存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续。</p>
<ul>
<li>对于问题1的第二种情况既然说到了排队等候机制那么就一定会有某种队列形成这样的队列是什么数据结构呢</li>
<li>处于排队等候机制中的线程,什么时候可以有机会获取锁呢?</li>
<li>如果处于排队等候机制中的线程一直无法获取锁,还是需要一直等待吗,还是有别的策略来解决这一问题?</li>
</ul>
<p>带着非公平锁的这些问题,再看下公平锁源码中获锁的方式:</p>
<pre><code>// java.util.concurrent.locks.ReentrantLock#FairSync
static final class FairSync extends Sync {
...
final void lock() {
acquire(1);
}
...
}
</code></pre>
<p>看到这块代码我们可能会存在这种疑问Lock函数通过Acquire方法进行加锁但是具体是如何加锁的呢</p>
<p>结合公平锁和非公平锁的加锁流程虽然流程上有一定的不同但是都调用了Acquire方法而Acquire方法是FairSync和UnfairSync的父类AQS中的核心方法。</p>
<p>对于上边提到的问题其实在ReentrantLock类源码中都无法解答而这些问题的答案都是位于Acquire方法所在的类AbstractQueuedSynchronizer中也就是本文的核心——AQS。下面我们会对AQS以及ReentrantLock和AQS的关联做详细介绍相关问题答案会在2.3.5小节中解答)。</p>
<h2>2 AQS</h2>
<p>首先我们通过下面的架构图来整体了解一下AQS框架</p>
<p><img src="assets/82077ccf14127a87b77cefd1ccf562d3253591.png" alt="img" /></p>
<ul>
<li>上图中有颜色的为Method无颜色的为Attribution。</li>
<li>总的来说AQS框架共分为五层自上而下由浅入深从AQS对外暴露的API到底层基础数据。</li>
<li>当有自定义同步器接入时只需重写第一层所需要的部分方法即可不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时先经过第一层的API进入AQS内部方法然后经过第二层进行锁的获取接着对于获取锁失败的流程进入第三层和第四层的等待队列处理而这些处理方式均依赖于第五层的基础数据提供层。</li>
</ul>
<p>下面我们会从整体到细节从流程到方法逐一剖析AQS框架主要分析过程如下</p>
<p><img src="assets/d2f7f7fffdc30d85d17b44266c3ab05323338.png" alt="img" /></p>
<h3>2.1 原理概览</h3>
<p>AQS核心思想是如果被请求的共享资源空闲那么就将当前请求资源的线程设置为有效的工作线程将共享资源设置为锁定状态如果共享资源被占用就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的将暂时获取不到锁的线程加入到队列中。</p>
<p>CLHCraig、Landin and Hagersten队列是单向链表AQS中的队列是CLH变体的虚拟双向队列FIFOAQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。</p>
<p>主要原理图如下:</p>
<p><img src="assets/7132e4cef44c26f62835b197b239147b18062.png" alt="img" /></p>
<p>AQS使用一个Volatile的int类型的成员变量来表示同步状态通过内置的FIFO队列来完成资源获取的排队工作通过CAS完成对State值的修改。</p>
<h4>2.1.1 AQS数据结构</h4>
<p>先来看下AQS中最基本的数据结构——NodeNode即为上面CLH变体队列中的节点。</p>
<p><img src="assets/960271cf2b5c8a185eed23e98b72c75538637.png" alt="img" /></p>
<p>解释一下几个方法和属性值的含义:</p>
<table>
<thead>
<tr>
<th align="left">方法和属性值</th>
<th align="left">含义</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">waitStatus</td>
<td align="left">当前节点在队列中的状态</td>
</tr>
<tr>
<td align="left">thread</td>
<td align="left">表示处于该节点的线程</td>
</tr>
<tr>
<td align="left">prev</td>
<td align="left">前驱指针</td>
</tr>
<tr>
<td align="left">predecessor</td>
<td align="left">返回前驱节点没有的话抛出npe</td>
</tr>
<tr>
<td align="left">nextWaiter</td>
<td align="left">指向下一个处于CONDITION状态的节点由于本篇文章不讲述Condition Queue队列这个指针不多介绍</td>
</tr>
<tr>
<td align="left">next</td>
<td align="left">后继指针</td>
</tr>
</tbody>
</table>
<p>线程两种锁的模式:</p>
<table>
<thead>
<tr>
<th align="left">模式</th>
<th align="left">含义</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">SHARED</td>
<td align="left">表示线程以共享的模式等待锁</td>
</tr>
<tr>
<td align="left">EXCLUSIVE</td>
<td align="left">表示线程正在以独占的方式等待锁</td>
</tr>
</tbody>
</table>
<p>waitStatus有下面几个枚举值</p>
<table>
<thead>
<tr>
<th align="left">枚举</th>
<th align="left">含义</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">0</td>
<td align="left">当一个Node被初始化的时候的默认值</td>
</tr>
<tr>
<td align="left">CANCELLED</td>
<td align="left">为1表示线程获取锁的请求已经取消了</td>
</tr>
<tr>
<td align="left">CONDITION</td>
<td align="left">为-2表示节点在等待队列中节点线程等待唤醒</td>
</tr>
<tr>
<td align="left">PROPAGATE</td>
<td align="left">为-3当前线程处在SHARED情况下该字段才会使用</td>
</tr>
<tr>
<td align="left">SIGNAL</td>
<td align="left">为-1表示线程已经准备好了就等资源释放了</td>
</tr>
</tbody>
</table>
<h4>2.1.2 同步状态State</h4>
<p>在了解数据结构后接下来了解一下AQS的同步状态——State。AQS中维护了一个名为state的字段意为同步状态是由Volatile修饰的用于展示当前临界资源的获锁情况。</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;
</code></pre>
<p>下面提供了几个访问这个字段的方法:</p>
<table>
<thead>
<tr>
<th align="left">方法名</th>
<th align="left">描述</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">protected final int getState()</td>
<td align="left">获取State的值</td>
</tr>
<tr>
<td align="left">protected final void setState(int newState)</td>
<td align="left">设置State的值</td>
</tr>
<tr>
<td align="left">protected final boolean compareAndSetState(int expect, int update)</td>
<td align="left">使用CAS方式更新State</td>
</tr>
</tbody>
</table>
<p>这几个方法都是Final修饰的说明子类中无法重写它们。我们可以通过修改State字段表示的同步状态来实现多线程的独占模式和共享模式加锁过程</p>
<p><img src="assets/27605d483e8935da683a93be015713f331378.png" alt="img" /><img src="assets/3f1e1a44f5b7d77000ba4f9476189b2e32806.png" alt="img" /></p>
<p>对于我们自定义的同步工具需要自定义获取同步状态和释放状态的方式也就是AQS架构图中的第一层API层。</p>
<h2>2.2 AQS重要方法与ReentrantLock的关联</h2>
<p>从架构图中可以得知AQS提供了大量用于自定义同步器实现的Protected方法。自定义同步器实现的相关方法也只是为了通过修改State字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法ReentrantLock需要实现的方法如下并不是全部</p>
<table>
<thead>
<tr>
<th align="left">方法名</th>
<th align="left">描述</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">protected boolean isHeldExclusively()</td>
<td align="left">该线程是否正在独占资源。只有用到Condition才需要去实现它。</td>
</tr>
<tr>
<td align="left">protected boolean tryAcquire(int arg)</td>
<td align="left">独占方式。arg为获取锁的次数尝试获取资源成功则返回True失败则返回False。</td>
</tr>
<tr>
<td align="left">protected boolean tryRelease(int arg)</td>
<td align="left">独占方式。arg为释放锁的次数尝试释放资源成功则返回True失败则返回False。</td>
</tr>
<tr>
<td align="left">protected int tryAcquireShared(int arg)</td>
<td align="left">共享方式。arg为获取锁的次数尝试获取资源。负数表示失败0表示成功但没有剩余可用资源正数表示成功且有剩余资源。</td>
</tr>
<tr>
<td align="left">protected boolean tryReleaseShared(int arg)</td>
<td align="left">共享方式。arg为释放锁的次数尝试释放资源如果释放后允许唤醒后续等待结点返回True否则返回False。</td>
</tr>
</tbody>
</table>
<p>一般来说自定义同步器要么是独占方式要么是共享方式它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式如ReentrantReadWriteLock。ReentrantLock是独占锁所以实现了tryAcquire-tryRelease。</p>
<p>以非公平锁为例这里主要阐述一下非公平锁与AQS之间方法的关联之处具体每一处核心方法的作用会在文章后面详细进行阐述。</p>
<p><img src="assets/b8b53a70984668bc68653efe9531573e78636.png" alt="img" /></p>
<p>为了帮助大家理解ReentrantLock和AQS之间方法的交互过程以非公平锁为例我们将加锁和解锁的交互流程单独拎出来强调一下以便于对后续内容的理解。</p>
<p><img src="assets/7aadb272069d871bdee8bf3a218eed8136919.png" alt="img" /></p>
<p>加锁:</p>
<ul>
<li>通过ReentrantLock的加锁方法Lock进行加锁操作。</li>
<li>会调用到内部类Sync的Lock方法由于Sync#lock是抽象方法根据ReentrantLock初始化选择的公平锁和非公平锁执行相关内部类的Lock方法本质上都会执行AQS的Acquire方法。</li>
<li>AQS的Acquire方法会执行tryAcquire方法但是由于tryAcquire需要自定义同步器实现因此执行了ReentrantLock中的tryAcquire方法由于ReentrantLock是通过公平锁和非公平锁内部类实现的tryAcquire方法因此会根据锁类型不同执行不同的tryAcquire。</li>
<li>tryAcquire是获取锁逻辑获取失败后会执行框架AQS的后续逻辑跟ReentrantLock自定义同步器无关。</li>
</ul>
<p>解锁:</p>
<ul>
<li>通过ReentrantLock的解锁方法Unlock进行解锁。</li>
<li>Unlock会调用内部类Sync的Release方法该方法继承于AQS。</li>
<li>Release中会调用tryRelease方法tryRelease需要自定义同步器实现tryRelease只在ReentrantLock中的Sync实现因此可以看出释放锁的过程并不区分是否为公平锁。</li>
<li>释放成功后所有处理由AQS框架完成与自定义同步器无关。</li>
</ul>
<p>通过上面的描述大概可以总结出ReentrantLock加锁解锁时API层核心方法的映射关系。</p>
<p><img src="assets/f30c631c8ebbf820d3e8fcb6eee3c0ef18748.png" alt="img" /></p>
<h2>2.3 通过ReentrantLock理解AQS</h2>
<p>ReentrantLock中公平锁和非公平锁在底层是相同的这里以非公平锁为例进行分析。</p>
<p>在非公平锁中,有一段这样的代码:</p>
<pre><code>// java.util.concurrent.locks.ReentrantLock
static final class NonfairSync extends Sync {
...
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
...
}
</code></pre>
<p>看一下这个Acquire是怎么写的</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) &amp;&amp; acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
</code></pre>
<p>再看一下tryAcquire方法</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
</code></pre>
<p>可以看出这里只是AQS的简单实现具体获取锁的实现方法是由各自的公平锁和非公平锁单独实现的以ReentrantLock为例。如果该方法返回了True则说明当前线程获取锁成功就不用往后执行了如果获取失败就需要加入到等待队列中。下面会详细解释线程是何时以及怎样被加入进等待队列中的。</p>
<h3>2.3.1 线程加入等待队列</h3>
<h4>2.3.1.1 加入队列的时机</h4>
<p>当执行Acquire(1)时会通过tryAcquire获取锁。在这种情况下如果获取锁失败就会调用addWaiter加入到等待队列中去。</p>
<h4>2.3.1.2 如何加入队列</h4>
<p>获取锁失败后会执行addWaiter(Node.EXCLUSIVE)加入等待队列,具体实现方法如下:</p>
<pre><code>// java.util.concurrent.locks.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;
}
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
</code></pre>
<p>主要的流程如下:</p>
<ul>
<li>通过当前的线程和锁模式新建一个节点。</li>
<li>Pred指针指向尾节点Tail。</li>
<li>将New中Node的Prev指针指向Pred。</li>
<li>通过compareAndSetTail方法完成尾节点的设置。这个方法主要是对tailOffset和Expect进行比较如果tailOffset的Node和Expect的Node地址是相同的那么设置Tail的值为Update的值。</li>
</ul>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
static {
try {
stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField(&quot;state&quot;));
headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField(&quot;head&quot;));
tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField(&quot;tail&quot;));
waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField(&quot;waitStatus&quot;));
nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField(&quot;next&quot;));
} catch (Exception ex) {
throw new Error(ex);
}
}
</code></pre>
<p>从AQS的静态代码块可以看出都是获取一个对象的属性相对于该对象在内存当中的偏移量这样我们就可以根据这个偏移量在对象内存当中找到这个属性。tailOffset指的是tail对应的偏移量所以这个时候会将new出来的Node置为当前队列的尾节点。同时由于是双向链表也需要将前一个节点指向尾节点。</p>
<ul>
<li>如果Pred指针是Null说明等待队列中没有元素或者当前Pred指针和Tail指向的位置不同说明被别的线程已经修改就需要看一下Enq的方法。</li>
</ul>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
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;
}
}
}
}
</code></pre>
<p>如果没有被初始化需要进行初始化一个头结点出来。但请注意初始化的头结点并不是当前线程节点而是调用了无参构造函数的节点。如果经历了初始化或者并发导致队列中有元素则与之前的方法相同。其实addWaiter就是一个在双端链表添加尾节点的操作需要注意的是双端链表的头结点是一个无参构造函数的头结点。</p>
<p>总结一下,线程获取锁的时候,过程大体如下:</p>
<ol>
<li>当没有线程获取到锁时线程1获取锁成功。</li>
<li>线程2申请锁但是锁被线程1占有。</li>
</ol>
<p><img src="assets/e9e385c3c68f62c67c8d62ab0adb613921117.png" alt="img" /></p>
<ol>
<li>如果再有线程要获取锁,依次在队列中往后排队即可。</li>
</ol>
<p>回到上边的代码hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回False说明当前线程可以争取共享资源如果返回True说明队列中存在有效节点当前线程必须加入到等待队列中。</p>
<pre><code>// java.util.concurrent.locks.ReentrantLock
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &amp;&amp; ((s = h.next) == null || s.thread != Thread.currentThread());
}
</code></pre>
<p>看到这里我们理解一下h != t &amp;&amp; ((s = h.next) == null || s.thread != Thread.currentThread());为什么要判断的头结点的下一个节点?第一个节点储存的数据是什么?</p>
<blockquote>
<p>双向链表中第一个节点为虚节点其实并不存储任何信息只是占位。真正的第一个有数据的节点是在第二个节点开始的。当h != t时 如果(s = h.next) == null等待队列正在有线程进行初始化但只是进行到了Tail指向Head没有将Head指向Tail此时队列中有元素需要返回True这块具体见下边代码分析。 如果(s = h.next) != null说明此时队列中至少有一个有效节点。如果此时s.thread == Thread.currentThread()说明等待队列的第一个有效节点中的线程与当前线程相同那么当前线程是可以获取资源的如果s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列。</p>
</blockquote>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer#enq
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
</code></pre>
<p>节点入队不是原子操作所以会出现短暂的head != tail此时Tail指向最后一个节点而且Tail指向Head。如果Head没有指向Tail可见5、6、7行这种情况下也需要将相关线程加入队列中。所以这块代码是为了解决极端情况下的并发问题。</p>
<h4>2.3.1.3 等待队列中线程出队列时机</h4>
<p>回到最初的源码:</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) &amp;&amp; acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
</code></pre>
<p>上文解释了addWaiter方法这个方法其实就是把对应的线程以Node的数据结构形式加入到双端队列里返回的是一个包含该线程的Node。而这个Node会作为参数进入到acquireQueued方法中。acquireQueued方法可以对排队中的线程进行“获锁”操作。</p>
<p>总的来说一个线程获取锁失败了被放入等待队列acquireQueued会把放入队列中的线程不断去获取锁直到获取成功或者不再需要获取中断</p>
<p>下面我们从“何时出队列”和“如何出队列”两个方向来分析一下acquireQueued源码</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
final boolean acquireQueued(final Node node, int arg) {
// 标记是否成功拿到资源
boolean failed = true;
try {
// 标记等待过程中是否中断过
boolean interrupted = false;
// 开始自旋,要么获取锁,要么中断
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果p是头结点说明当前节点在真实数据队列的首部就尝试获取锁别忘了头结点是虚节点
if (p == head &amp;&amp; tryAcquire(arg)) {
// 获取锁成功头指针移动到当前node
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 说明p为头节点且当前没有获取到锁可能是非公平锁被抢占了或者是p不为头结点这个时候就要判断当前node是否要被阻塞被阻塞条件前驱节点的waitStatus为-1防止无限循环浪费资源。具体两个方法下面细细分析
if (shouldParkAfterFailedAcquire(p, node) &amp;&amp; parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
</code></pre>
<p>setHead方法是把当前节点置为虚节点但并没有修改waitStatus因为它是一直需要用的数据。</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer
// 靠前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取头结点的节点状态
int ws = pred.waitStatus;
// 说明头结点处于唤醒状态
if (ws == Node.SIGNAL)
return true;
// 通过枚举值我们知道waitStatus&gt;0是取消状态
if (ws &gt; 0) {
do {
// 循环向前查找取消节点,把取消节点从队列中剔除
node.prev = pred = pred.prev;
} while (pred.waitStatus &gt; 0);
pred.next = node;
} else {
// 设置前任节点等待状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
</code></pre>
<p>parkAndCheckInterrupt主要用于挂起当前线程阻塞调用栈返回当前线程的中断状态。</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
</code></pre>
<p>上述方法的流程图如下:</p>
<p><img src="assets/c124b76dcbefb9bdc778458064703d1135485.png" alt="img" /></p>
<p>从上图可以看出跳出当前循环的条件是当“前置节点是头结点且当前线程获取锁成功”。为了防止因死循环导致CPU资源被浪费我们会判断前置节点的状态来决定是否要将当前线程挂起具体挂起流程用流程图表示如下shouldParkAfterFailedAcquire流程</p>
<p><img src="assets/9af16e2481ad85f38ca322a225ae737535740.png" alt="img" /></p>
<p>从队列中释放节点的疑虑打消了,那么又有新问题了:</p>
<ul>
<li>shouldParkAfterFailedAcquire中取消节点是怎么生成的呢什么时候会把一个节点的waitStatus设置为-1</li>
<li>是在什么时间释放节点通知到被挂起的线程呢?</li>
</ul>
<h3>2.3.2 CANCELLED状态节点生成</h3>
<p>acquireQueued方法中的Finally代码</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
...
for (;;) {
final Node p = node.predecessor();
if (p == head &amp;&amp; tryAcquire(arg)) {
...
failed = false;
...
}
...
} finally {
if (failed)
cancelAcquire(node);
}
}
</code></pre>
<p>通过cancelAcquire方法将Node的状态标记为CANCELLED。接下来我们逐行来分析这个方法的原理</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
private void cancelAcquire(Node node) {
// 将无效节点过滤
if (node == null)
return;
// 设置该节点不关联任何线程,也就是虚节点
node.thread = null;
Node pred = node.prev;
// 通过前驱节点跳过取消状态的node
while (pred.waitStatus &gt; 0)
node.prev = pred = pred.prev;
// 获取过滤后的前驱节点的后继节点
Node predNext = pred.next;
// 把当前node的状态设置为CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点
// 更新失败的话则进入else如果更新成功将tail的后继节点设置为null
if (node == tail &amp;&amp; compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 如果当前节点不是head的后继节点1:判断当前节点前驱节点的是否为SIGNAL2:如果不是则把前驱节点设置为SINGAL看是否成功
// 如果1和2中有一个为true再判断当前节点的线程是否为null
// 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点
if (pred != head &amp;&amp; ((ws = pred.waitStatus) == Node.SIGNAL || (ws &lt;= 0 &amp;&amp; compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &amp;&amp; pred.thread != null) {
Node next = node.next;
if (next != null &amp;&amp; next.waitStatus &lt;= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 如果当前节点是head的后继节点或者上述条件不满足那就唤醒当前节点的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
</code></pre>
<p>当前的流程:</p>
<ul>
<li>获取当前节点的前驱节点如果前驱节点的状态是CANCELLED那就一直往前遍历找到第一个waitStatus &lt;= 0的节点将找到的Pred节点和当前Node关联将当前Node设置为CANCELLED。</li>
<li>根据当前节点的位置,考虑以下三种情况:</li>
</ul>
<p>(1) 当前节点是尾节点。</p>
<p>(2) 当前节点是Head的后继节点。</p>
<p>(3) 当前节点不是Head的后继节点也不是尾节点。</p>
<p>根据上述第二条,我们来分析每一种情况的流程。</p>
<p>当前节点是尾节点。</p>
<p><img src="assets/b845211ced57561c24f79d56194949e822049.png" alt="img" /></p>
<p>当前节点是Head的后继节点。</p>
<p><img src="assets/ab89bfec875846e5028a4f8fead32b7117975.png" alt="img" /></p>
<p>当前节点不是Head的后继节点也不是尾节点。</p>
<p><img src="assets/45d0d9e4a6897eddadc4397cf53d6cd522452.png" alt="img" /></p>
<p>通过上面的流程我们对于CANCELLED节点状态的产生和变化已经有了大致的了解但是为什么所有的变化都是对Next指针进行了操作而没有对Prev指针进行操作呢什么情况下会对Prev指针进行操作</p>
<blockquote>
<p>执行cancelAcquire的时候当前节点的前置节点可能已经从队列中出去了已经执行过Try代码块中的shouldParkAfterFailedAcquire方法了如果此时修改Prev指针有可能会导致Prev指向另一个已经移除队列的Node因此这块变化Prev指针不安全。 shouldParkAfterFailedAcquire方法中会执行下面的代码其实就是在处理Prev指针。shouldParkAfterFailedAcquire是获取锁失败的情况下才会执行进入该方法后说明共享资源已被获取当前节点之前的节点都不会出现变化因此这个时候变更Prev指针比较安全。</p>
<pre><code>do {
node.prev = pred = pred.prev;
} while (pred.waitStatus &gt; 0);
</code></pre>
</blockquote>
<h3>2.3.3 如何解锁</h3>
<p>我们已经剖析了加锁过程中的基本流程接下来再对解锁的基本流程进行分析。由于ReentrantLock在解锁的时候并不区分公平锁和非公平锁所以我们直接看解锁的源码</p>
<pre><code>// java.util.concurrent.locks.ReentrantLock
public void unlock() {
sync.release(1);
}
</code></pre>
<p>可以看到,本质释放锁的地方,是通过框架来完成的。</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null &amp;&amp; h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
</code></pre>
<p>在ReentrantLock里面的公平锁和非公平锁的父类Sync定义了可重入锁的释放锁机制。</p>
<pre><code>// java.util.concurrent.locks.ReentrantLock.Sync
// 方法返回当前锁是不是没有被线程持有
protected final boolean tryRelease(int releases) {
// 减少可重入次数
int c = getState() - releases;
// 当前线程不是持有锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果持有线程全部释放将当前独占锁所有线程设置为null并更新state
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
</code></pre>
<p>我们来解释下述源码:</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final boolean release(int arg) {
// 上边自定义的tryRelease如果返回true说明该锁没有被任何线程持有
if (tryRelease(arg)) {
// 获取头结点
Node h = head;
// 头结点不为空并且头结点的waitStatus不是初始化节点情况解除线程挂起状态
if (h != null &amp;&amp; h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
</code></pre>
<p>这里的判断条件为什么是h != null &amp;&amp; h.waitStatus != 0</p>
<blockquote>
<p>h == null Head还没初始化。初始情况下head == null第一个节点入队Head会被初始化一个虚拟节点。所以说这里如果还没来得及入队就会出现head == null 的情况。</p>
<p>h != null &amp;&amp; waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。</p>
<p>h != null &amp;&amp; waitStatus &lt; 0 表明后继节点可能被阻塞了,需要唤醒。</p>
</blockquote>
<p>再看一下unparkSuccessor方法</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
private void unparkSuccessor(Node node) {
// 获取头结点waitStatus
int ws = node.waitStatus;
if (ws &lt; 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取当前节点的下一个节点
Node s = node.next;
// 如果下个节点是null或者下个节点被cancelled就找到队列最开始的非cancelled的节点
if (s == null || s.waitStatus &gt; 0) {
s = null;
// 就从尾部节点开始找到队首找到队列第一个waitStatus&lt;0的节点。
for (Node t = tail; t != null &amp;&amp; t != node; t = t.prev)
if (t.waitStatus &lt;= 0)
s = t;
}
// 如果当前节点的下个节点不为空,而且状态&lt;=0就把当前节点unpark
if (s != null)
LockSupport.unpark(s.thread);
}
</code></pre>
<p>为什么要从后往前找第一个非Cancelled的节点呢原因如下。</p>
<p>之前的addWaiter方法</p>
<pre><code>// java.util.concurrent.locks.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;
}
</code></pre>
<p>我们从这里可以看到节点入队并不是原子操作也就是说node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作但是此时pred.next = node;还没执行如果这个时候执行了unparkSuccessor方法就没办法从前往后找了所以需要从后往前找。还有一点原因在产生CANCELLED状态节点的时候先断开的是Next指针Prev指针并未断开因此也是必须要从后往前遍历才能够遍历完全部的Node。</p>
<p>综上所述如果是从前往后找由于极端情况下入队的非原子操作和CANCELLED节点产生过程中断开Next指针的操作可能会导致无法遍历所有的节点。所以唤醒对应的线程后对应的线程就会继续往下执行。继续执行acquireQueued方法以后中断如何处理</p>
<h3>2.3.4 中断恢复后的执行流程</h3>
<p>唤醒后会执行return Thread.interrupted();,这个函数返回的是当前执行线程的中断状态,并清除。</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
</code></pre>
<p>再回到acquireQueued代码当parkAndCheckInterrupt返回True或者False的时候interrupted的值不同但都会执行下次循环。如果这个时候获取锁成功就会把当前interrupted返回。</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head &amp;&amp; tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &amp;&amp; parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
</code></pre>
<p>如果acquireQueued为True就会执行selfInterrupt方法。</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
</code></pre>
<p>该方法其实是为了中断线程。但为什么获取了锁以后还要中断线程呢这部分属于Java提供的协作式中断知识内容感兴趣同学可以查阅一下。这里简单介绍一下</p>
<ol>
<li>当中断线程被唤醒时并不知道被唤醒的原因可能是当前线程在等待中被中断也可能是释放了锁以后被唤醒。因此我们通过Thread.interrupted()方法检查中断标记该方法返回了当前线程的中断状态并将当前线程的中断标识设置为False并记录下来如果发现该线程被中断过就再中断一次。</li>
<li>线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止。也就是说,在整个流程中,并不响应中断,只是记录中断记录。最后抢到锁返回了,那么如果被中断过的话,就需要补充一次中断。</li>
</ol>
<p>这里的处理方式主要是运用线程池中基本运作单元Worder中的runWorker通过Thread.interrupted()进行额外的判断处理感兴趣的同学可以看下ThreadPoolExecutor源码。</p>
<h3>2.3.5 小结</h3>
<p>我们在1.3小节中提出了一些问题,现在来回答一下。</p>
<blockquote>
<p>Q某个线程获取锁失败的后续流程是什么呢</p>
<p>A存在某种排队等候机制线程继续等待仍然保留获取锁的可能获取锁流程仍在继续。</p>
<p>Q既然说到了排队等候机制那么就一定会有某种队列形成这样的队列是什么数据结构呢</p>
<p>A是CLH变体的FIFO双端队列。</p>
<p>Q处于排队等候机制中的线程什么时候可以有机会获取锁呢</p>
<p>A可以详细看下2.3.1.3小节。</p>
<p>Q如果处于排队等候机制中的线程一直无法获取锁需要一直等待么还是有别的策略来解决这一问题</p>
<p>A线程所在节点的状态会变成取消状态取消状态的节点会从队列中释放具体可见2.3.2小节。</p>
<p>QLock函数通过Acquire方法进行加锁但是具体是如何加锁的呢</p>
<p>AAQS的Acquire会调用tryAcquire方法tryAcquire由各个自定义同步器实现通过tryAcquire完成加锁过程。</p>
</blockquote>
<h2>3 AQS应用</h2>
<h3>3.1 ReentrantLock的可重入应用</h3>
<p>ReentrantLock的可重入性是AQS很好的应用之一在了解完上述知识点以后我们很容易得知ReentrantLock实现可重入的方法。在ReentrantLock里面不管是公平锁还是非公平锁都有一段逻辑。</p>
<p>公平锁:</p>
<pre><code>// java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire
if (c == 0) {
if (!hasQueuedPredecessors() &amp;&amp; compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc &lt; 0)
throw new Error(&quot;Maximum lock count exceeded&quot;);
setState(nextc);
return true;
}
</code></pre>
<p>非公平锁:</p>
<pre><code>// java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
if (c == 0) {
if (compareAndSetState(0, acquires)){
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc &lt; 0) // overflow
throw new Error(&quot;Maximum lock count exceeded&quot;);
setState(nextc);
return true;
}
</code></pre>
<p>从上面这两段都可以看到有一个同步状态State来控制整体可重入的情况。State是Volatile修饰的用于保证一定的可见性和有序性。</p>
<pre><code>// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;
</code></pre>
<p>接下来看State这个字段主要的过程</p>
<ol>
<li>State初始化的时候为0表示没有任何线程持有锁。</li>
<li>当有线程持有该锁时,值就会在原来的基础上+1同一个线程多次获得锁是就会多次+1这里就是可重入的概念。</li>
<li>解锁也是对这个字段-1一直到0此线程对锁释放。</li>
</ol>
<h3>3.2 JUC中的应用场景</h3>
<p>除了上边ReentrantLock的可重入性的应用AQS作为并发编程的框架为很多其他同步工具提供了良好的解决方案。下面列出了JUC中的几种同步工具大体介绍一下AQS的应用场景</p>
<table>
<thead>
<tr>
<th align="left">同步工具</th>
<th align="left">同步工具与AQS的关联</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">ReentrantLock</td>
<td align="left">使用AQS保存锁重复持有的次数。当一个线程获取锁时ReentrantLock记录当前获得锁的线程标识用于检测是否重复获取以及错误线程试图解锁操作时异常情况的处理。</td>
</tr>
<tr>
<td align="left">Semaphore</td>
<td align="left">使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数acquireShared会减少计数。</td>
</tr>
<tr>
<td align="left">CountDownLatch</td>
<td align="left">使用AQS同步状态来表示计数。计数为0时所有的Acquire操作CountDownLatch的await方法才可以通过。</td>
</tr>
<tr>
<td align="left">ReentrantReadWriteLock</td>
<td align="left">使用AQS同步状态中的16位保存写锁持有的次数剩下的16位用于保存读锁的持有次数。</td>
</tr>
<tr>
<td align="left">ThreadPoolExecutor</td>
<td align="left">Worker利用AQS同步状态实现对独占线程变量的设置tryAcquire和tryRelease</td>
</tr>
</tbody>
</table>
<h3>3.3 自定义同步工具</h3>
<p>了解AQS基本原理以后按照上面所说的AQS知识点自己实现一个同步工具。</p>
<pre><code>public class LeeLock {
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire (int arg) {
return compareAndSetState(0, 1);
}
@Override
protected boolean tryRelease (int arg) {
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively () {
return getState() == 1;
}
}
private Sync sync = new Sync();
public void lock () {
sync.acquire(1);
}
public void unlock () {
sync.release(1);
}
}
</code></pre>
<p>通过我们自己定义的Lock完成一定的同步功能。</p>
<pre><code>public class LeeMain {
static int count = 0;
static LeeLock leeLock = new LeeLock();
public static void main (String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run () {
try {
leeLock.lock();
for (int i = 0; i &lt; 10000; i++) {
count++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
leeLock.unlock();
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
</code></pre>
<p>上述代码每次运行结果都会是20000。通过简单的几行代码就能实现同步功能这就是AQS的强大之处。</p>
<h2>总结</h2>
<p>我们日常开发中使用并发的场景太多但是对并发内部的基本框架原理了解的人却不多。由于篇幅原因本文仅介绍了可重入锁ReentrantLock的原理和AQS原理希望能够成为大家了解AQS和ReentrantLock等同步器的“敲门砖”。</p>
<h2>参考资料</h2>
<ul>
<li>Lea D. The java. util. concurrent synchronizer framework[J]. Science of Computer Programming, 2005, 58(3): 293-309.</li>
<li>《Java并发编程实战》</li>
<li><a href="https://tech.meituan.com/2018/11/15/java-lock.html">不可不说的Java“锁”事</a></li>
</ul>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/互联网并发限流实战.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/文章/从SpringCloud开始聊微服务架构.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709980371df78b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>