Java 多线程 —— AQS 详解

引言

AQS 是AbstractQuenedSynchronizer 的缩写,抽象的队列式同步器,它是除了java自带的synchronized关键字之外的锁机制。是 JUC 下的重要组件。

相关产物有:ReentrantLock、CountDownLatch、Semaphore、ReadWriteLock等。

一、AQS的设计思想

AbstractQuenedSynchronizer 维护了一个 volatile int state 变量,代表共享资源。

若state 是0,代表资源空闲,当前线程将 0 改为 1,表示上锁,当前线程置为工作线程;

若state不为0,代表资源占用,当前线程依然会 acquire() 一个资源,如果恰好是当前的工作线程,那么state 累加,以此描述“重入性”;如果当前线程并不是工作线程,则会被安置在一个由AQS维护的资源等待队列。

AQS队列会让第一个线程Node自旋获取资源,而后面的线程,则通过 LockSupport.park(this) 方法将线程置为 WAITING 状态等待被唤醒。

如果第一个线程获取到了资源,那么就将它设置为队列的 head 节点,原 head 就会被移出队列。

AQS的设计中用到了模板方法模式,不同的资源共享机制如互斥或共享可以由子类自定义实现:

Exclusive:如ReentrantLock、
Share:如信号量、闭锁、读写锁等。

AQS 的另一个特点是自旋+CAS。

在请求资源和入列等操作中,经常会看到 for(;;) 、compareAndSetState、compareAndSetTail等操作,这与synchronized的实现有很大区别。

通过比较并设置的方式,可以有效提高资源获取的效率,但同时也会消耗额外的CPU资源。

二、两种资源访问策略的代表

在AQS中维护了一个 Node 节点,它有两种等待模式,同时也表示资源的两种不同的访问策略:

/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;

由此,衍生出两类不同的子类实现,第一类是以ReentrantLock为代表的互斥锁,它在语义上与synchronized实现了相同的互斥性和可重入性;另一类是以闭锁CountDownLatch 为代表的线程同步工具。

2.1 ReentrantLock

首先 AQS 中的 state 为 0.

A 线程在执行 lock() 方法后,以独占方式 CAS state 为 1。AQS 会记录 A 线程为当前的独占线程,其他线程如果再尝试获取资源,就会进入等待队列,直到 A 线程调用 unlock() 方法,释放了资源,即 state 回归 0 状态。

在“重入性”方面,如果A线程第二次尝试取锁,state 会累加。也就是说,上锁的次数一定等于释放锁的次数。

2.2 CountDownLatch

CountDownLatch 翻译为 “闭锁”或“门闩”,这是一种非常好用的同步工具,可以延迟线程的进度直到终止状态。

与 ReentrantLock 不同的是,在构造 CountDownLatch 对象的时候,会先设定一个 state 大小:

CountDownLatch latch = new CountDownLatch(3);

这个 3 就是 state 变量的初始值,然后线程使用 countDown() 方法递减这个计数,直到 state = 0,放行所有 waiting 中的线程。

CountDownLatch 维护的 state 表示的是事件数量,当指定数量的事件执行完毕后,就会 unpark() 主调线程,继续后续动作。

在使用CountDownLatch时有一个误区是,state 的值就代表了线程的数量,认为我 state = 3 ,就需要 3 个线程去执行任务,其实,state = 10 也依然可以使用 一个线程去执行,关键要区分事件与并行任务的概念。

三、ReentrantLock 源码

作为补充 synchronized 的锁机制,ReentrantLock 显示锁的功能非常强大,但这里不打算全面分析ReentrantLock的奇技淫巧,而是从 lock() 方法出发,分析一下 AQS 是如何实现资源的锁定和等待队列的维护的。

3.1 acquire

acquire 是 AQS 的顶层入口,他表示获取锁资源。

public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

Doug Lea 的代码非常简洁,根本没有一句废话,就连代码结构也非常精简,如果是分析源码的话,我们可以尝试去改写一下这个方法,让其可读性更强一些:

public final void acquire(int arg) {if (!tryAcquire(arg)) {Node newWaiter = addWaiter(Node.EXCLUSIVE);boolean needInterrupt = acquireQueued(newWaiter, arg);if (needInterrupt) {selfInterrupt();}}
}

从方法中的一系列方法名和判断逻辑来看。

尝试获取资源,如果成功,则直接返回。如果不成功,addWaiter 添加一个独占模式的等待者,acquireQueued 以排队的方式去获取资源。

3.2 tryAcquire

tryAcquire 在 ReentrantLock 中有两种实现,分别是:

FairSync 中的公平锁实现;

NonfairSync 中的非公平锁实现

当然,公平与非公平并不是重点,就以非公平的实现来看一下,

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) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

当前线程会 CAS state 0->1,或累加重入,成功返回true,失败返回false。

3.3 addWaiter

在acquire上锁操作失败后,会执行这个方法:

private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;
}

addWaiter ,添加一个等待者,它只完成一项工作,就是向等待队列中添加一个 Node:

1、将当前线程封装为一个队列 Node;

2、取得队列的尾节点 tail,并CAS 新的节点设置为新的 tail

3、设置新 tail 成功,直接返回

4、若设置新 tail 不成功,或者干脆,原tail 就不存在,执行 enq 方法,自旋操作以上步骤,直到成功。

enq方法是 enqueue 的缩写,意思是“使队列化”,它就是一个 while-true ,如果队列不存在,就创建一个队列,如果队列已经存在,就把 node 放到最后一个:

private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}

这里就用到了自旋操作,每次自旋都会获取当前的 tail 节点,避免在设置的过程中间被其他线程加塞,却又不知道。

刚进入方法的时候,肯定需要走初始化的逻辑,这会创建一个 空的 Node 节点作为 head,所以由此我们也知道,AQS 队列中的头结点实际上就是一个没有实际意义的功能型节点,里边是没有线程的,真正封装了线程的节点是从第二个节点开始。

总体来看,addWaiter 完全就是一个 do-while 循环,先执行一次 CASTail,失败后循环执行CASTail,直到成功后返回该 node,同时也是新的 tail 节点。

3.4 acquireQueued

在 addWaiter 添加了新的 tail 后,需要做哪些事情呢?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 GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

这里需要说明一下,该方法的逻辑兼顾了中断的操作,如果对中断机制不太了解,可以暂时不去理会。

该方法同样是一个 while-true 循环,当且仅当,当前节点是队列中第二个节点(addWaiter中已经很明确,AQS队列中的head 节点就是一个空的 Node),并且 tryAcquire 成功,才会返回。在返回之前,仅做了一些队列的维护工作:设置新的head 节点。

如果没有“当且仅当”,那么执行 park:

private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();
}

也就是说,除了第二个节点以后的节点,都要进行 park,即线程切换为 WAITING 状态。

四、AQS acquire 流程

经过了上一节的源码分析,我们已经大概清楚了 lock() 方法调用之后发生的事情,接下来就需要总结一下 acquire 流程步骤,提炼一下 AQS 队列的工作原理:

总结

AQS 使用了大量的 CAS 操作,避免上锁,你在ReentrantLock中看不到一句 synchronized 。

通过CAS 和自旋的配合可以一定程度上提高同步代码的性能。

state 以 volatile 类型修饰,可以在多线程之间提供可见性。

ReentrantLock 和 CountDownLatch 对 state 的访问方式分为独占和共享两种,本文虽然没有解析 CountDownLatch 的源码,但通过上面源码的分析,可以想到其大致实现流程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/558592.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

的主机名_如何在Mac 上更改电脑的名称或本地局域网主机名?

我们知道&#xff0c;一台电脑有其设定的具体名称&#xff0c;电脑的名称和本地主机名用于在本地网络上识别您的电脑。当我们需要自定义电脑名称或本地局域网主机名时&#xff0c;则需要对其进行更改。那我们该如何更改呢&#xff1f;有需要的小伙伴们快和小编一起来看看吧~更改…

dev控件swiftplot图滚动方法_无限轮播图使用Scroller就这么简单

前言这几天又拾起老本行&#xff0c;复习复习Android&#xff0c;才发现忘的差不多了&#xff0c;上午做了一个小Demo&#xff0c;配合Scroller做了一个轮播图&#xff0c;效果如下&#xff0c;但是不知为何&#xff0c;录制的GIF成这样&#xff0c;凑乎一下看看。原理是继承Vi…

JVM——CPU缓存架构与Java 内存模型

导航一、CPU缓存架构与一致性协议1.1 CPU缓存架构1.2 缓存行与伪共享问题1.3 MESI 缓存一致性协议1.4 伪共享的解决办法二、JMM Java 内存模型2.1 JMM 简介2.2 原子性、可见性、有序性2.3 八大内存交互操作2.4 happens-before 原则一、CPU缓存架构与一致性协议 1.1 CPU缓存架构…

白噪声检测_科学家尝试用智能扬声器的白噪声来监测婴儿的呼吸运动

华盛顿大学的一支研究团队&#xff0c;刚刚介绍了他们开发的一种新型智能扬声器技术。这种设备能够借助白噪声来安抚熟睡的婴儿&#xff0c;并监测他们的呼吸和运动。具体说来是&#xff0c;通过智能扬声器发出的白噪声&#xff0c;原型设备能够将之与生命体征监测仪的数据相匹…

最大值_285期 博最大值2路,已经箭在弦上!

往期数据P-5掉码 跨度 和尾 012断路 余数和 位数86072 1 8 4 200 断1路2 5 对214对 双双双79703 0 2 3 101 断2路2 4 对215对 单单单62386 0 4 1 020 断1路2 4 错216对 双双单71903 0 8 7 110 断2路2 5 错217对 单单单64838 0 4 8 012 来3路3 4 错218对 双双双02052 0 2 2 020 …

商品领域ddd_为 Gopher 打造 DDD 系列:领域模型-资源库

前言&#xff1a; 作为领域模型中最重要的环节之一的Repository&#xff0c;其通过对外暴露接口屏蔽了内部的复杂性&#xff0c;又有其隐式写时复制的巧妙代码设计&#xff0c;完美的将DDD中的Repository的概念与代码相结合&#xff01;Repository资源库通常标识一个存储的区域…

mysql5.7主从全备恢复_Mysql5.7—运维常用备份方式(超全)

小生博客&#xff1a;http://xsboke.blog.51cto.com小生 Q Q&#xff1a;1770058260-------谢谢您的参考&#xff0c;如有疑问&#xff0c;欢迎交流一、 Mysqldump备份结合binlog日志恢复使用mysqldump进行全库备份&#xff0c;并使用binlog日志备份&#xff0c;还原时&#xf…

docker 运行容器_Docker之运行 Django 容器

首先此篇笔记默认你已经安装好了 Docker&#xff0c;并了解 Docker 的基础概念&#xff0c;诸如镜像、容器、以及他们之间的关系等。如果不太了解&#xff0c;等我回头了解清楚以后&#xff0c;可以再写一篇文章阐述一下。&#xff08;狗头当然&#xff0c;对于这篇文章&#x…

mysql8.0与mysql7.0_MySQL 5.7 vs 8.0,哪个性能更牛?

测试mysql5.7和mysql8.0分别在读写&#xff0c;选定&#xff0c;只写模式下不同并发时的性能(tps&#xff0c;qps)最早测试使用版本为mysql5.7.22和mysql8.0.15sysbench测试前先重启mysql服务&#xff0c;并清除os的缓存(避免多次测试时命中缓存)每次进行测试都是新生成测试数据…

springmvc使用requestmapping无法访问控制类_研究人员称人类使用的新烟碱类杀虫剂让蜜蜂无法入睡...

来自布里斯托尔大学的科学家进行了研究&#xff0c;显示常见的杀虫剂可以阻止蜜蜂和苍蝇睡个好觉。就像人类一样&#xff0c;许多昆虫也需要睡眠才能正常工作。然而&#xff0c;如果它们接触过新烟碱类杀虫剂&#xff0c;它们的睡眠就会受到影响&#xff0c;新烟碱类杀虫剂是一…

linux 监控mysql脚本_Linux系统MySQL主从同步监控shell脚本

操作系统&#xff1a;CentOS系统目的&#xff1a;定时监控MySQL数据库主从是否同步&#xff0c;如果不同步&#xff0c;记录故障时间&#xff0c;并执行命令使主从恢复同步状态1、创建脚本文件vi /home/crontab/check_mysql_slave.sh #编辑&#xff0c;添加下面代码#!/bin/sh…

python协成_Python协程(上)

几个概念&#xff1a;event_loop 事件循环&#xff1a;程序开启一个无限的循环&#xff0c;程序员会把一些函数注册到事件循环上。当满足事件发生的时候&#xff0c;调用相应的协程函数。coroutine 协程&#xff1a;协程对象&#xff0c;指一个使用async关键字定义的函数&#…

js父元素获取子元素img_css,前端_父标签div中包含一个子元素img标签,子元素div标签,为什么img要加上浮动,子元素div才会处于正常位置?,css,前端 - phpStudy...

父标签div中包含一个子元素img标签&#xff0c;子元素div标签&#xff0c;为什么img要加上浮动&#xff0c;子元素div才会处于正常位置&#xff1f;dom结构如图img加上float 子元素div显示正常。不加float div显示错位。附上我写的一个dome测试用的&#xff0c;大家可本地看下究…

android运营商获取本机号码_一键登录已成大势所趋,Android端操作指南来啦!

根据极光(Aurora Mobile)发布的《2019年Q2移动互联网行业数据研究报告》&#xff0c;2019年第二季度&#xff0c;移动网民人均安装APP总量已达56款。面对如此繁多的APP&#xff0c;想在用户的手机中占据一席之地&#xff0c;移动开发者们就不得不努力提升用户体验。而现实却是&…

spring批量写入mysql数据库_MyBatis-spring和spring JDBC批量插入Mysql的效率比较

工具框架用spring-batch&#xff0c;数据库是mysql(未做特殊优化)。比较数据框架mybatis和spring jdbc的插入效率。Mybatis三种实现&#xff1a;1、mybatis的官方写法Java代码publicvoidbatchInsert1(List poilist)throwsException {SqlSession sqlSession sqlSessionFactory.…

金额转换java_java金额转换

像商品价格&#xff0c;订单&#xff0c;结算都会涉及到一些金额的问题&#xff0c;为了避免精度丢失通常会做一些处理&#xff0c;常规的系统中金额一般精确到小数点后两位&#xff0c;也就是分&#xff1b;这样数据库在设计的时候金额就直接存储整型数据类型&#xff0c;前端…

java bloomfilter_爬虫技术之——bloom filter(含java代码)

在爬虫系统中&#xff0c;在内存中维护着两个关于URL的队列&#xff0c;ToDo队列和Visited队列&#xff0c;ToDo队列存放的是爬虫从已经爬取的网页中解析出来的即将爬取的URL&#xff0c;但是网页是互联的&#xff0c;很可能解析出来的URL是已经爬取到的&#xff0c;因此需要VI…

java php js_【javascript/PHP】当一个JavaScripter初次进入PHP的世界,他将看到这样的风景...

本文将从以下11点介绍javascript和PHP在基础语法和基本操作上的异同&#xff1a;1.数据类型的异同2.常量和变量的定义的不同&#xff0c;字符串连接运算符不同3.对象的创建方法的不同4.PHP与JS在变量声明提升和函数声明提升的差异5.var在JS和PHP中使用的差异6.PHP和JS在访问对象…

从零开始学java 框架_从零开始学 Java - 搭建 Spring MVC 框架

如果创建一个 Spring 项目Spring MVC 框架在 Java 的 Web 项目中应该是无人不知的吧&#xff0c;你不会搭建一个 Spring 框架&#xff1f;作为身为一个刚刚学习Java的我都会&#xff0c;如果你不会的话&#xff0c;那可真令人忧伤。1.在 MyEclipse 创建项目后&#xff0c;可以以…

java 系统类型_Java获取操作系统类型

Java获取操作系统完整版系统枚举类&#xff1a;public enum EPlatform {Any("any"),Linux("Linux"),Mac_OS("Mac OS"),Mac_OS_X("Mac OS X"),Windows("Windows"),OS2("OS/2"),Solaris("Solaris"),SunOS…