LongAdder为什么在高并发下保持良好性能?LongAdder源码详细分析

文章目录

  • 一、LongAdder概述
    • 1、为什么用LongAdder
    • 2、LongAdder使用
    • 3、LongAdder继承关系图
    • 4、总述:LongAdder为什么这么快
    • 5、基本原理
  • 二、Striped64源码分析
    • 1、Striped64重要概念
    • 2、Striped64常用变量或方法
    • 3、静态代码块初始化UNSAFE
    • 4、casBase方法
    • 5、casCellsBusy方法
    • 6、getProbe方法
    • 7、longAccumulate方法
  • 三、深入分析LongAdder的核心add方法
    • 1、单线程更新LongAdder的值
    • 2、多线程竞争创建cells数组
    • 3、有了Cells之后,再次进行add
    • 4、总结
  • 四、LongAdder的sum方法求和
  • 参考资料

一、LongAdder概述

1、为什么用LongAdder

《阿里巴巴Java开发手册中》:

【参考】 volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
说明: 如果是 count++操作,使用如下类实现: AtomicInteger count = new AtomicInteger();
count.addAndGet(1); 如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。

在低并发下,LongAdder和AtomicLong具有相似的特征。但在高并发下,LongAdder的预期吞吐量要高得多,但代价是空间消耗更高

2、LongAdder使用

Java-Atomic原子操作类详解及源码分析,Java原子操作类进阶,LongAdder源码分析

3、LongAdder继承关系图

LongAdder继承了Striped64,而Striped64同样也是JUC包下一员。LongAdder有着这么特殊的特性,是离不开Striped64的。
在这里插入图片描述
在这里插入图片描述

4、总述:LongAdder为什么这么快

LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前的AtomicLong一个value的更新压力分散到多个value中区,从而降级更新热点。

这也是“分段锁”的实现思想。
在这里插入图片描述

5、基本原理

LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用分散热点的做法,用空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和base都加起来作为最终结果。(注:多个线程是有可能操作同一个cell的,因为其hash映射有可能相同)

二、Striped64源码分析

1、Striped64重要概念

在这里插入图片描述

2、Striped64常用变量或方法

base:类似于AtomicLong中全局的value值,在没有竞争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上。

collide:表示扩容意向,false一定不会扩容,true可能扩容。

cellsBusy:初始化cells或者扩容cells需要获取锁,0表示无所状态,1表示其他线程已经持有了锁。

casCellsBusy():通过CAS操作修改cellsBusy的值,CAS成功表示获取锁,返回true。

NCPU:当前计算机CPU数量,Cell数组扩容时会使用到。

getProbe():获取当前线程的hash值。

advanceProbe():重置当前线程的hash值。

3、静态代码块初始化UNSAFE

下面的源码中我们可以看出,Striped64在静态代码块中初始化了Unsafe类,并且初始化了Unsafe类的对于对象属性的更新,其中包括base、cellsBusy、threadLocalRandomProbe,用于线程安全的更新Striped64的属性。

// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long BASE;
private static final long CELLSBUSY;
private static final long PROBE;
static {try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class<?> sk = Striped64.class;BASE = UNSAFE.objectFieldOffset(sk.getDeclaredField("base"));CELLSBUSY = UNSAFE.objectFieldOffset(sk.getDeclaredField("cellsBusy"));Class<?> tk = Thread.class;PROBE = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomProbe"));} catch (Exception e) {throw new Error(e);}
}

4、casBase方法

casBase用于CAS更新base字段,通过预期值来更新Striped64中的base字段:

// java.util.concurrent.atomic.Striped64#casBase
finalboolean casBase(long cmp, long val) {return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}

5、casCellsBusy方法

casCellsBusy():通过CAS操作修改cellsBusy的值,CAS成功表示获取锁,返回true。

// java.util.concurrent.atomic.Striped64#casCellsBusy
final boolean casCellsBusy() {return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}

6、getProbe方法

该方法获取线程的hash值(probe值)

static final int getProbe() {return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

7、longAccumulate方法

final void longAccumulate(long x, LongBinaryOperator fn,boolean wasUncontended) {// 存储线程的probe值int h;// 如果getProbe() 为0 ,说明随机数未初始化(极端情况)if ((h = getProbe()) == 0) {// 使用ThreadLocalRandom 为当前线程重新计算一个hash值,强制初始化ThreadLocalRandom.current(); // force initialization// 重新获取probe值, hash值被重置就好比一个全新的线程一样,所以设置了wasUncontended竞争状态为trueh = getProbe();// 重新计算了当前线程的hash后,认为此次不算是一次竞争,都未初始化,肯定还不存在竞争激烈,wasUncontended竞争状态设为truewasUncontended = true;}boolean collide = false;                // True if last slot nonempty// 自选,共分三个分支for (;;) {Cell[] as; Cell a; int n; long v;// CASE1:表示cells数组已经被初始化了if ((as = cells) != null && (n = as.length) > 0) {// ...}// CASE2:cells数组没有加锁且没有初始化,则尝试对它进行加锁,并初始化else if (cellsBusy == 0 && cells == as && casCellsBusy()) {// ...}// CASE3:兜底,cells正在初始化,则尝试直接在基数base上进行累加else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))break;                          // Fall back on using base}
}

三、深入分析LongAdder的核心add方法

1、单线程更新LongAdder的值

// java.util.concurrent.atomic.LongAdder#add
public void add(long x) {/**as表示cells(Striped64的cells数组)的引用;b表示获取的Striped64的base属性;v表示当前线程hash到Cell中存储的值;m表示cells数组的长度-1,hash时作为掩码使用;a表示当前线程命中的cell单元格*/Cell[] as; long b, v; int m; Cell a;// cells是Striped64中的cells,初始是null// 当没有线程竞争时,casBase方法更新base值,是可以更新成功的,if条件不成立,方法执行完成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);}
}

上面我们分析到,当单线程更新LongAdder的值时,由于没有线程竞争,直接通过cas更新base的值,更新成功后方法直接结束。

2、多线程竞争创建cells数组

public void add(long x) {Cell[] as; long b, v; int m; Cell a;// 当多个线程执行casBase时,会有可能cas失败,此时就进入if逻辑if ((as = cells) != null || !casBase(b = base, b + x)) {// uncontended默认为true,无竞争,false表示竞争激烈,多个线程hash到同一个Cell,可能要扩容boolean uncontended = true;// 首次进来时,as为null,进入longAccumulate方法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);}
}

longAccumulate方法较长,for(;;)中,有个if:

if ((as = cells) != null && (n = as.length) > 0) {
// ...// CASE2:cells数组没有加锁且没有初始化,则尝试对它进行加锁,并初始化
// cellsBusy初始就是0, cells == as == null,并且casCellsBusy拿到锁定
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {boolean init = false;try {                           // Initialize tableif (cells == as) { // 双重检查,保证线程安全// 创建Cell[2]数组Cell[] rs = new Cell[2];// 计算下标,初始化一个cell,初始值为xrs[h & 1] = new Cell(x);cells = rs;init = true;}} finally {cellsBusy = 0;}if (init)break;
}

到此,我们知道,当有线程cas竞争之后,会初始化2个长度的Cell数组,并创建一个Cell。

3、有了Cells之后,再次进行add

public void add(long x) {Cell[] as; long b, v; int m; Cell a;// cells 不再为null了,而是2个长度的Cell数组if ((as = cells) != null || !casBase(b = base, b + x)) {boolean uncontended = true;// as 不为null// as.length 为2 (m = as.length - 1) < 0也不成立,正常不会成立,相当于as == null的兜底// as[getProbe() & m]) 表示cells数组的该槽位为null,还没初始化,就会执行longAccumulate初始化一个Cell// 如果上述还不成立,a.cas(v = a.value, v + x) 直接执行Cell中的value的cas操作,如果成功就退出,如果cas失败就执行longAccumulate,并且将cas的结果赋值给uncontendedif (as == null || (m = as.length - 1) < 0 ||(a = as[getProbe() & m]) == null ||!(uncontended = a.cas(v = a.value, v + x)))longAccumulate(x, null, uncontended);}
}
for (;;) {Cell[] as; Cell a; int n; long v;// CASE1:cells已经被初始化了if ((as = cells) != null && (n = as.length) > 0) {// if总结:判断当前线程hash后指向的数据位置元素是否为空,为空则将Cell数据放入数组跳出循环,不为空则继续循环if ((a = as[(n - 1) & h]) == null) { // 当前线程的hash值运算后映射得到的Cell单元为null,说明该Cell没有被使用if (cellsBusy == 0) {       // Try to attach new Cell Cell[]数组没有正在扩容(没有锁)Cell r = new Cell(x);   // Optimistically create 创建一个Cell单元if (cellsBusy == 0 && casCellsBusy()) { // 尝试加锁,成功后cellsBusy == 1boolean created = false;try {               // Recheck under lock 在有锁的情况下再进行一次检查Cell[] rs; int m, j; // 将Cell单元附到Cell[]数组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;}// wasUncontended表示cells初始化后,当前线程竞争修改失败// 若wasUncontended = false,这里只是重新设置了这个值为true,紧接着执行advanceProbe(h)重置当前线程hash,重新循环else if (!wasUncontended)       // CAS already known to failwasUncontended = true;      // Continue after rehash// 说明当前线程对应的数组中有了数据,也重置过hash值,这时通过CAS操作尝试对当前数中的value值进行累加x操作,如果CAS成功则直接跳出循环else if (a.cas(v = a.value, ((fn == null) ? v + x :fn.applyAsLong(v, x))))break;// 如果n大于CPU最大容量,不可扩容,紧接着执行advanceProbe(h)重置当前线程hash,重新循环else if (n >= NCPU || cells != as)collide = false;            // At max size or stale// 如果扩容意向collide是false,则修改它为true,然后执行advanceProbe(h)重置当前线程hash,重新循环// 如果当前数组长度已经大于了CPU核数,就会再次设置扩容意向collide = false (见上一步)else if (!collide)collide = true;// 加锁、扩容else if (cellsBusy == 0 && casCellsBusy()) {try { // 当前的cells数组和最先赋值的as是同一个,表示没有被其他线程扩容过if (cells == as) {      // Expand table unless staleCell[] rs = new Cell[n << 1]; // 按位左移1位,扩容大小为之前容量的2倍for (int i = 0; i < n; ++i)rs[i] = as[i]; // 扩容后再将之前数组的元素拷贝到新数组cells = rs;}} finally {cellsBusy = 0; // 释放锁}collide = false; // 设置扩容状态,继续循环continue;                   // Retry with expanded table}// 兜底:重置当前线程的hash,重新循环h = advanceProbe(h);}else if (cellsBusy == 0 && cells == as && casCellsBusy()) {// ...

4、总结

在这里插入图片描述

四、LongAdder的sum方法求和

sum()会将所有Cell数组中的value和base累加作为返回值。在没有并发更新的情况下调用将返回准确的结果,但在计算总和时发生的并发更新可能不会合并。

sum执行时,并没有限制对base和cells的更新。所以LongAdder不是强一致性的,它是最终一致性的。

首先,最终返回的sum局部变量,初始被赋值为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。其次,这里对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; // base + 所有cell中的value}}return sum;
}

参考资料

https://zxbcw.cn/post/214652/

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

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

相关文章

如何利用验证链技术减少大型语言模型中的幻觉

一、前言 随着大型语言模型在自然语言处理领域取得了惊人的进步。相信深度使用过大模型产品的朋友都会发现一个问题&#xff0c;就是有时候在上下文内容比较多&#xff0c;对话比较长&#xff0c;或者是模型本身知识不了解的情况下与GPT模型对话&#xff0c;模型反馈出来的结果…

阿里云服务器续费流程_一篇文章搞定

阿里云服务器如何续费&#xff1f;续费流程来了&#xff0c;在云服务器ECS管理控制台选择续费实例、续费时长和续费优惠券&#xff0c;然后提交订单&#xff0c;分分钟即可完成阿里云服务器续费流程&#xff0c;阿里云服务器网aliyunfuwuqi.com分享阿里云服务器详细续费方法&am…

微信扫一扫抽奖活动怎么做

在当今数字化时代&#xff0c;微信作为中国最大的社交媒体平台之一&#xff0c;拥有着庞大的用户群体和广泛的影响力。微信扫一扫抽奖活动作为一种创新的营销方式&#xff0c;可以利用微信的用户基础和社交属性&#xff0c;吸引更多的目标用户参与&#xff0c;提高品牌知名度和…

鸿蒙状态栏设置

鸿蒙状态栏设置 基于鸿蒙 ArkTS API9&#xff0c;设置状态栏颜色&#xff0c;隐藏显示状态栏。 API参考文档 参考文档 新建项目打开之后发现状态栏是黑色的&#xff0c;页面颜色设置完了也不能影响状态栏颜色&#xff0c;如果是浅色背景&#xff0c;上边有个黑色的头&#…

众和策略:题材股什么意思?

题材股是股票商场上的一个术语&#xff0c;许多刚接触股票出资的人可能对它不太熟悉。那么&#xff0c;题材股什么意思呢&#xff1f;在本文中&#xff0c;咱们将从多个角度剖析这个问题&#xff0c;帮忙读者更好地了解。 一、什么是题材股 题材股是指某个工作或主题的股票集结…

机器学习笔记 - 深度学习中跳跃连接的直观解释

一、概述 如今人们利用深度学习做无数的应用。然而,为了理解在许多作品中看到的大量设计选择(例如跳过连接),了解一点反向传播机制至关重要。 如果你在 2014 年尝试训练神经网络,你肯定会观察到所谓的梯度消失问题。简单来说:你在屏幕后面检查网络的训练过程,你看到的只…

跨越单线程限制:Thread类的魅力,引领你进入Java并发编程的新纪元

线程的概述 线程是一个程序的多个执行路径&#xff0c;执行调度的单位&#xff0c;依托于进程存在。 线程不仅可以共享进程的内存&#xff0c;而且还拥有一个属于自己的内存空间&#xff0c;这段内存空间也叫做线程栈&#xff0c;是在建立线程时由系统分配的&#xff0c;主要用…

【C++】不是用new生成的对象调用析构函数

2023年10月23日&#xff0c;周一上午 #include <iostream>class Book{ private:int price; public:~Book(){std::cout<<"调用析构函数"<<std::endl; } };int main(){Book b1;b1.~Book(); } 从运行结果可以看出&#xff1a; 手动调用b1.~Book()时&…

机器人系统 ROS 常用命令行工具

1. 启动ros 主节点 roscore roscore运行成功如图&#xff1a; 1.1 rosrun 启动服务节点 例子&#xff1a;启动一个小乌龟节点 rosrun turtlesim turtlesim_node运行结果如图&#xff1a; 1.2 启动键盘控制 打开新的命令窗口&#xff0c;启动turtle_teleop_key 节点 rosr…

单窗口单IP适合炉石传说游戏么?

游戏道具制作在炉石传说中是一个很有挑战的任务&#xff0c;但与此同时&#xff0c;它也是一个充满机遇的领域。在这篇文章中&#xff0c;我们将向您展示如何在炉石传说游戏中使用动态包机、多窗口IP工具和动态IP进行游戏道具制作。 作者与主题的关系&#xff1a;作为一名热爱炉…

JSX看着一篇足以入门

JSX 介绍 学习目标&#xff1a; 能够理解什么是 JSX&#xff0c;JSX 的底层是什么 概念&#xff1a; JSX 是 javaScriptXML(HTML) 的缩写&#xff0c;表示在 JS 代码中书写 HTML 结构 作用&#xff1a; 在 React 中创建 HTML 结构&#xff08;页面 UI 结构&#xff09; 优势&a…

VM虚拟机 13.5 for Mac

VMware Fusion Pro for Mac是一款强大的虚拟机软件&#xff0c;可以在Mac操作系统中创建、运行和管理多个虚拟机&#xff0c;使用户可以在一台Mac电脑上同时运行多个操作系统和应用程序。 以下是VMware Fusion Pro for Mac的主要特点&#xff1a; 1. 支持多种操作系统&#xff…

【数据结构】线性表(九)队列:链式队列及其基本操作(初始化、判空、入队、出队、存取队首元素)

文章目录 一、队列1. 定义2. 基本操作 二、顺序队列三、链式队列0. 链表1. 头文件2. 队列结构体3. 队列的初始化4. 判断队列是否为空5. 入队6. 出队7. 存取队首元素8. 主函数9. 代码整合 堆栈Stack 和 队列Queue是两种非常重要的数据结构&#xff0c;两者都是特殊的线性表&…

【Java 进阶篇】深入浅出:Bootstrap 轮播图

在现代网页设计中&#xff0c;轮播图是一个常见的元素。它们可以用于展示图片、广告、新闻、产品或任何您希望吸引用户注意力的内容。要实现一个轮播图&#xff0c;您通常需要一些复杂的HTML、CSS和JavaScript代码&#xff0c;这对于初学者来说可能会感到困难。但幸运的是&…

React环境初始化

环境初始化 学习目标&#xff1a; 能够独立使用React脚手架创建一个React项目 1.使用脚手架创建项目 官方文档&#xff1a;(https://create-react-app.bootcss.com/)    - 打开命令行窗口    - 执行命令      npx create-react-app projectName    说明&#xff1a…

四、网络请求与路由

一、网络请求 1、Axios请求 Axios是一个基于promise的网络请求库 &#xff08;1&#xff09;安装 npm install --save axios&#xff08;2&#xff09;引入 import axios from "axios"全局引入 import axios from "axios" import { createApp } from …

靶机 DC_1

DC_1 信息搜集 存活检测 详细扫描 网页目录扫描 网页信息搜集 cms 为 Drupal 漏洞利用 使用 msf 搜索 drupal 的漏洞 启动 msfconsole搜索 search drupal尝试编号为 0 的漏洞 失败 利用编号为 1 的漏洞 use 1查看需要配置的选项 show options设置目标 ip set rhost 10…

【Linxu工具】:vim使用及简单配置

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关Linux工具&#xff1a;vim的使用&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从…

不知道怎么选CRM系统?看这篇就够了

CRM客户管理系统近年来已经从简单的客户管理软件发展成为了帮助企业运营发展的工具。它能够帮助企业优化业务流程、提高客户转化率、获得更多业绩。那么企业在选择CRM系统时有什么要点吗&#xff1f; 1、明确是否有自动化功能 自动化功能可以自动处理那些手动且琐碎的销售流程…

【Docker从入门到入土 4】使用Harbor搭建Docker私有仓库

私有仓库 一、Harbor简介1.1 什么是Harbor?1.2 Harbor的特性1.3 Harbor和docker registry的关系1.4 Harbor的构成1.4 Harbor 配置文件中的两类参数1.4.1 所需参数1.4.2 可选参数 二、Harbor部署2.1 部署Docker-Compose服务2.2 部署 Harbor 服务Step1 下载或上传 Harbor 安装程…