剑指JUC原理-10.并发编程大师的原子累加器底层优化原理(与人类的优秀灵魂对话)

  • 👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
  • 📕系列专栏:Spring源码、JUC源码
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:源码溯源,一探究竟
  • 📝联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

文章目录

    • 累加器性能比较
    • 源码之 LongAdder
      • 原理之伪共享
      • LongAdder源码

累加器性能比较

private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {T adder = adderSupplier.get();long start = System.nanoTime();List<Thread> ts = new ArrayList<>();// 4 个线程,每人累加 50 万for (int i = 0; i < 40; i++) {ts.add(new Thread(() -> {for (int j = 0; j < 500000; j++) {action.accept(adder);}}));}ts.forEach(t -> t.start());ts.forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long end = System.nanoTime();System.out.println(adder + " cost:" + (end - start)/1000_000);}

比较 AtomicLong 与 LongAdder

for (int i = 0; i < 5; i++) {demo(() -> new LongAdder(), adder -> adder.increment());
}
for (int i = 0; i < 5; i++) {demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
}

输出

20000000 cost:68
20000000 cost:8
20000000 cost:7
20000000 cost:7
20000000 cost:2220000000 cost:352
20000000 cost:240
20000000 cost:327
20000000 cost:338
20000000 cost:321

第一次运行看不出来,jvm加入虚拟机内部,程序被反复执行后,才会做出优化,执行一次,效果看不出来。

性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加
Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性
能。换句话说,核心数越多,提升性能越明显

源码之 LongAdder

LongAdder 是并发大师 @author Doug Lea (大哥李)的作品,设计的非常精巧

LongAdder 类有几个关键域

// 累加单元数组, 懒惰初始化(多线程去累加的时候,每个线程用各自的一个Cell累加单元来累加,可以减少从试,从而提高性能)
transient volatile Cell[] cells;// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;

cas 锁

public class LockCas {private AtomicInteger state = new AtomicInteger(0);public void lock() {while (true) {if (state.compareAndSet(0, 1)) {break;}}}public void unlock() {log.debug("unlock...");state.set(0);}
}

0 没加锁
1 加锁

第一个线程,相当于将0 变成 1。此时第二个线程,再将0 变成 1,那就加锁失败了。会一直while(true)

测试

LockCas lock = new LockCas();
new Thread(() -> {log.debug("begin...");lock.lock();try {log.debug("lock...");sleep(1);} finally {lock.unlock();}
}).start();
new Thread(() -> {log.debug("begin...");lock.lock();try {log.debug("lock...");} finally {lock.unlock();}
}).start();

输出

18:27:07.198 c.Test42 [Thread-0] - begin... 
18:27:07.202 c.Test42 [Thread-0] - lock... 
18:27:07.198 c.Test42 [Thread-1] - begin... 
18:27:08.204 c.Test42 [Thread-0] - unlock... 
18:27:08.204 c.Test42 [Thread-1] - lock... 
18:27:08.204 c.Test42 [Thread-1] - unlock... 

原理之伪共享

其中 Cell 即为累加单元

// 防止缓存行伪共享
@sun.misc.Contended
static final class Cell {volatile long value;Cell(long x) { value = x; }// 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值final boolean cas(long prev, long next) {return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);}// 省略不重要代码
}

得从缓存说起

缓存与内存的速度比较

在这里插入图片描述

在这里插入图片描述

因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。

缓存以缓存行为单位每个缓存行对应着一块内存,一般是 64 byte(8 个 long) 重点!!!

缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中

CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效

在这里插入图片描述

因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因
此缓存行可以存下 2 个的 Cell 对象。这样问题来了:

  • Core-0 要修改 Cell[0]
  • Core-1 要修改 Cell[1]

无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加
Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效

@sun.misc.Contended (实际上就是防止 一个缓存行容纳 多个 Cell对象)用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。

在这里插入图片描述

LongAdder源码

在这里插入图片描述

累加主要调用下面的方法

public void add(long x) {Cell[] as; long b, v; int m; Cell a;if ((as = cells) != null || !casBase(b = base, b + x)) {boolean uncontended = true;if (as == null || (m = as.length - 1) < 0 ||(a = as[getProbe() & m]) == null ||!(uncontended = a.cas(v = a.value, v + x)))longAccumulate(x, null, uncontended);}}

从if开始分析,cells是累加单元的数组,判断为空还是不为空,cells数组是懒惰创建的,没有竞争的时候是null,竞争发生的时候才会去创建cells数组。从而再创建里面的cell累加单元,一开始先判断是否有竞争,如果没竞争,那么一开始是为空的。后半部分是对基础累加值进行累加,这里显然是cas操作,使用casBase对基础的域进行累加。如果成功了就不会进入到if块,如果基础累加值失败了,那么就进入到 里面的if块。

此时cells还是为空,那么就直接进入了longAccumulate 方法中。这里面主要会涉及到cells、cell的创建。

如果cells不为空,代表着以前发生过竞争了,需要去判断当前的线程有没有一个对应的Cell被创建了,如果是null就代表没有没有被创建,代表 (a = as[getProbe() & m]) == null 这个条件直接成立,直接就会进入 longAccumulate 方法

如果当前条件不成立,当前线程有一个累加单元了,其中a就是累加单元,那么就执行累加单元的cas,成功了,就不会进入 longAccumulate 方法了

add 流程图

在这里插入图片描述

final void longAccumulate(long x, LongBinaryOperator fn,boolean wasUncontended) {int h;if ((h = getProbe()) == 0) {ThreadLocalRandom.current(); // force initializationh = getProbe();wasUncontended = true;}boolean collide = false;                // True if last slot nonemptyfor (;;) { // 相当于while(true)Cell[] as; Cell a; int n; long v;if ((as = cells) != null && (n = as.length) > 0) { ... } // celss数组不为空的情况else if (cellsBusy == 0 && cells == as && casCellsBusy()) { // cells为空的情况boolean init = false;try {                           // Initialize tableif (cells == as) {Cell[] rs = new Cell[2];rs[h & 1] = new Cell(x);cells = rs;init = true;}} finally {cellsBusy = 0;}if (init)break;}else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))break;                          // Fall back on using base}}

首先先研究 cells数组为空的情况

cellsBusy首先是一个标记位,0代表未加锁,1代表加锁,因为我们要创建cells数组需要为其加锁,如果其他线程如果也想创建这个数组就会产生了冲突。

后面 cells == as 代表还没有其他线程去改变这个 cells ,也许有其他线程也在去尝试去创建cells,创建成功了,就会将其复制给cells变量,as是最初读到的数组的引用,cells有可能是别的线程创建出来的新的数组的引用,所以需要对比一下,对比的目的就是看有没有别人修改过这个引用。

casCellsBusy() 其实将cellsBusy从0变为1,使用cas操作去尝试去加锁,这个方法相当于图中加锁的逻辑。

如果加锁失败就进入到了 和它平级的else if里了,去base上使用cas进行累加,如果base累加成功了,就return,如果失败了就重回循环。

进来以后,再次判断看看其他线程是否将cells创建好了,后续创建一个大小为2的累加单元数组,然后创建累加单元,然后赋值cells。

这里面需要注意就是虽然cells大小是2,但是累加单元cell只创建了一个,还有一个是空着的,这也是懒惰初始化的关键,不到万不得已不会创建的。

最终cellsBusy设置为0,解锁。

longAccumulate 流程图

在这里插入图片描述

for (;;) {Cell[] as; Cell a; int n; long v;if ((as = cells) != null && (n = as.length) > 0) { // cells创建好了,cell还没有创建if ((a = as[(n - 1) & h]) == null) {if (cellsBusy == 0) {       // Try to attach new CellCell r = new Cell(x);   // Optimistically createif (cellsBusy == 0 && casCellsBusy()) {boolean created = false;try {               // Recheck under lockCell[] rs; int m, j;if ((rs = cells) != null &&(m = rs.length) > 0 &&rs[j = (m - 1) & h] == null) {rs[j] = r;created = true;}} finally {cellsBusy = 0;}if (created)break;continue;           // Slot is now non-empty}}collide = false;}...}else if (cellsBusy == 0 && cells == as && casCellsBusy()) {...}else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))break;                          // Fall back on using base}

cells创建好了,但是线程对应的cell还没有创建,因为刚才也看到了,cells创建出来,只给当前那个线程创建了累加单元,假设我是另一个线程,那么未必累加单元对象就创建好了,因此,我们要为这个线程在没有对应的累加单元的时候,将累加单元给它创建出来,累加单元是用到时才创建。

定位到 if ((a = as[(n - 1) & h]) == null) 这段代码,实际上获取当前线程看看有没有对应的a的累加单元,如果为null,那么就还没有对应的累加单元。

首先创建了一个Cell对象,但是此时还没有存储到数组里面去,数组中肯定还有空位,等着放入累加单元。

首先根据cellsBusy判断是否上锁,如果没上锁就上锁。因为这是需要改数组内容,将新创建出来的累加单元设置到数组中的空着的地方,需要对数组上锁。如果上锁失败,就回到循环入口。

如果加锁成功,又做了一遍检查,再次确保数组是不为空的,数组的长度是不为零的,然后又检查这个线程对应的那个数组中空的槽位是不是真的是null,是null的话代表着这个cell对象没有别人创建,那么前面创建的cell对象就可以存入数组的下标中去,如果不为空,说明有其他线程在数组的该下标下创建了cell对象,那前面的cell对象就白创建了,就再次回到循环中。

成功了把锁解开,然后break;

在这里插入图片描述

if ((as = cells) != null && (n = as.length) > 0) {if ((a = as[(n - 1) & h]) == null) {...}else if (!wasUncontended)       // CAS already known to failwasUncontended = true;      // Continue after rehashelse if (a.cas(v = a.value, ((fn == null) ? v + x :fn.applyAsLong(v, x))))break;else if (n >= NCPU || cells != as)collide = false;            // At max size or staleelse if (!collide)collide = true;else if (cellsBusy == 0 && casCellsBusy()) {try {if (cells == as) {      // Expand table unless staleCell[] rs = new Cell[n << 1];for (int i = 0; i < n; ++i)rs[i] = as[i];cells = rs;}} finally {cellsBusy = 0;}collide = false;continue;                   // Retry with expanded table}h = advanceProbe(h); // 改变线程对应的cell}

如果cells存在并且cell创建好了,此时会进入到这行代码else if (a.cas(v = a.value, ((fn == null) ? v + x :fn.applyAsLong(v, x))))

累加单元就是a,调用a的cas方法,对原来的值进行累加,如果失败了,进入下面的else if,首先会检查是否超过了cpu上限,n就是数组的长度,看看n是否会大于cpu的个数,如果已经大于等于cpu个数的话,那么这个cells数组再扩容其实就没有意义了,其实也就是防止走后面的扩容逻辑。

如果没有超过cpu上线,那就走加锁逻辑,如果上锁失败了,那么其实就 改变线程对应的累加单元。

如果没有超过cpu上线,就会执行扩容逻辑,其实本质上是使用移位运算符创建一个二倍长度的数组,然后将原数组中的cell赋值过来即可,然后最终将新cells也赋值过来。

每个线程刚进入 longAccumulate 时,会尝试对应一个 cell 对象(找到一个坑位)

在这里插入图片描述

获取最终结果通过 sum 方法

	public long sum() {Cell[] as = cells; Cell a;long sum = base;if (as != null) {for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)sum += a.value;}}return sum;}

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

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

相关文章

flutter项目引入本地静态图片资源并展示

想要在flutter中引入静态资源&#xff0c;需要配置pubspec.yaml&#xff0c;将本地的静态资源添加到assets下面&#xff1a; 然后在flutter引入这些静态资源&#xff1a; Image.asset("images/squick.png") 就可以在app中看到这个图片了&#xff1a; 也可以使用网…

Pytorch从零开始实战08

Pytorch从零开始实战——YOLOv5-C3模块实现 本系列来源于365天深度学习训练营 原作者K同学 文章目录 Pytorch从零开始实战——YOLOv5-C3模块实现环境准备数据集模型选择开始训练可视化模型预测总结 环境准备 本文基于Jupyter notebook&#xff0c;使用Python3.8&#xff0c…

ubuntu 分区 方案

ubuntu 分区 方案 自动分区啥样子的&#xff1f; 手动分区 需要怎么操作&#xff1f; 注意点是啥&#xff1f; swap分区 要和 内存大小 差不多 安装ubuntu系统时硬盘分区方案 硬盘分区概述 一块硬盘最多可以分4个主分区&#xff0c;主分区之外的成为扩展分区。硬盘可以没有…

CTF工具PDF隐写神器wbStego4open安装和详细使用方法

wbStego4open安装和详细使用方法 1.wbStego4open介绍&#xff1a;2.wbStego4open下载&#xff1a;3.wbStego4open原理图&#xff1a;4.wbStego4open使用教程&#xff1a;第一步&#xff1a;第二步&#xff1a;第三步&#xff1a;第四步&#xff1a;第五步&#xff1a; 5.wbSteg…

基于ThinkPHP+MySQL实现的通用的PHP网站后台管理系统

caozha-admin 后台管理框架 1.8.3 caozha-admin是一个通用的PHP网站后台管理框架&#xff0c;基于开源的ThinkPHP开发&#xff0c;特点&#xff1a;易上手&#xff0c;零门槛&#xff0c;界面清爽极简&#xff0c;极便于二次开发。 基础功能 1、系统设置 2、管理员管理 3、…

蓝桥杯:买不到的数目

对于两个互质的正整数 n , m n,m n,m,请找出来不能被 n n n和 m m m组成的最大数 X X X 例如:对于4,7那么 X X X17&#xff0c;因为对于大于17的任一数都可由4和7组成。 重新翻译题目&#xff1a; 对于任一大于 X X X的正整数 Y Y Y满足 Y a n b m Y a \times nb \times m …

常见的单token登录方案

现在主流的单token方案为jwttoken和redis token 常用的跟jwt token集成框架有shrio、spring security、aop切面。redis也能跟这三者集成。跟redis相比&#xff0c;jwt token比较难注销&#xff0c;得等到有效期过了才行&#xff0c;实际根据项目需求来就行。 简单介绍如下&am…

二叉树OJ题汇总

本专栏内容为&#xff1a;leetcode刷题专栏&#xff0c;记录了leetcode热门题目以及重难点题目的详细记录 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;Leetcode &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&#x1f69a; &…

Nacos报错Connection refused (Connection refused)(最后原因醉了,非常醉)

目录 一、问题产生二、排查思路1.nacos拒绝连接&#xff0c;排查思路&#xff1a;2.Nacos启动成功但是拒绝连接的几种原因&#xff1a; 三、实操过程&#xff08;着急解决问题直接看这个&#xff09;1.启动Nacos2.查看Nacos启动日志3.根据日志处理问题4.修改Nacos5.重启Nacos 一…

HT5010 音频转换器工作原理

HT5010是一款低成B的立体声DA转换器&#xff0c;内部集成了内插滤波器、DA转换器和输出模拟滤波等电路。其可支持多种音频数字输入格式&#xff0c;支持24-bit字节。 该HT5010 基于一个多比特位的Δ-Σ调制器&#xff0c;将数字信号转化成两个声道的模拟信号并经过模拟滤波器滤…

天体学爱好者基础知识-太阳系//未完待续,业余者的学习

难过的时候&#xff0c;仰望天空吧&#xff0c;人类有时候&#xff0c;做的事情真的太愚昧且无聊了&#xff0c;渺小的尘埃&#xff0c;也可以飘际宇宙。 太阳系-八大行星 卫星围绕着恒星公转。行星必须围绕着恒星公转。 什么是行星&#xff1f;行星和恒星、卫星有什么区别&am…

Azure 机器学习 - 使用 Visual Studio Code训练图像分类 TensorFlow 模型

了解如何使用 TensorFlow 和 Azure 机器学习 Visual Studio Code 扩展训练图像分类模型来识别手写数字。 关注TechLead&#xff0c;分享AI全维度知识。作者拥有10年互联网服务架构、AI产品研发经验、团队管理经验&#xff0c;同济本复旦硕&#xff0c;复旦机器人智能实验室成员…

静态库的概念及影响

1、目标文件的生成&#xff1a; 由编译器针对源文件编译生成&#xff0c;生成的.o或者.so(动态库)或者.a(静态库)也可以看作是目标文件&#xff1b; 2、静态库的生成&#xff1a; 由给定的一堆目标文件以及链接选项&#xff0c;链接器可以生成两种库&#xff0c;分别是静态库…

学 Java 怎么进外企?

作者&#xff1a;**苍何&#xff0c;CSDN 2023 年 实力新星&#xff0c;前大厂高级 Java 工程师&#xff0c;阿里云专家博主&#xff0c;土木转码&#xff0c;现任部门技术 leader&#xff0c;专注于互联网技术分享&#xff0c;职场经验分享。 &#x1f525;热门文章推荐&#…

【教程】R语言生物群落(生态)数据统计分析与绘图

查看原文>>>R语言生物群落&#xff08;生态&#xff09;数据统计分析与绘图实践 暨融合《R语言基础》、《tidyverse数据清洗》、《多元统计分析》、《随机森林模型》、《回归及混合效应模型》、《结构方程模型》、《统计结果作图》七合一版本方案 R 语言作的开源、自…

httpclient工具类(支持泛型转换)

1、网上搜到的httpclient工具类的问题&#xff1a; 1.1、如下图我们都能够发现这种封装的问题&#xff1a; 代码繁杂、充斥了很多重复性代码返回值单一&#xff0c;无法拿到对应的Java Bean对象及List对象集合实际场景中会对接大量第三方的OPEN API&#xff0c;下述方法的扩展…

预处理详解(二)

1.宏和函数对比 宏通常被应用于执行简单的运算。 比如在两个数中找出较大的一个。 #define MAX(a, b) ((a)>(b)?(a):(b)) 那为什么不用函数来完成这个任务&#xff1f; 原因有二&#xff1a; 1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所…

Hadoop相关知识点

文章目录 一、主要命令二、配置虚拟机2.1 设置静态ip2.2 修改主机名及映射2.3 修改映射2.4 单机模式2.5 伪分布式2.6 完全分布式 三、初识Hadoop四、三种模式的区别4.1、单机模式与伪分布式模式的区别4.2、特点4.3、配置文件的差异4.3.1、单机模式4.3.2、伪分布式模式4.3.3、完…

ChatGPT 被爆重大隐私泄露!在回答时突然蹦出陌生男子自拍照,你的数据都将被偷走训练模型!

ChatGPT 被爆重大隐私泄露 &#xff01; 一位用户在向 ChatGPT 询问 Python 中的代码格式化包 black 的用法时&#xff0c;没有一点点防备&#xff0c;ChatGPT 在回答中插入了一个陌生男子的自拍照&#xff08;手动捂脸.jpg&#xff09; 可以看到刚开始 ChatGPT 还相当正常&am…

CentOS停更沉寂,RHEL巨变限制源代:Docker容器化技术的兴起助力操作系统新格局

一、概述 操作系统是计算机系统的核心软件&#xff0c;它管理和控制着计算机的硬件和软件资源&#xff0c;为用户和应用程序提供了一个统一、高效、安全的运行环境。操作系统的发展历史也是计算机技术的发展历史的重要组成部分&#xff0c;它见证了计算机从单机到网络&#xf…