【JUC】二十三、LongAdder:多线程计数的更优解

文章目录

  • 1、常用API
  • 2、热点商品点赞计算器
  • 3、LongAdder高性能的原理
  • 4、源码:LongAdder-add方法
  • 5、源码:LongAdder-longAccumulate方法
  • 6、源码:LongAdder-sum方法
  • 7、AtomicLong和LongAdder的对比

Since 1.8,新加原子操作增强类:

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

API文档:runoob.com/manual/jdk11api/java.base/java/util/concurrent/atomic/LongAdder.html

1、常用API

常用方法:
在这里插入图片描述

LongAdder只能用来计算加法,且从零开始计算,LongAccumulator则更灵活,可传入一个函数式接口和初始值,函数式接口中自定义计算逻辑,加减乘除。

LongAdder longAdder = new LongAdder();
longAdder.increment();  //+1
longAdder.increment();
longAdder.increment();
System.out.println(longAdder.sum());  //3
//new LongAccumulator((x, y) -> x * y, 1)
LongAccumulator longAccumulator = new LongAccumulator(new LongBinaryOperator() {
    @Override
    //left:初始值,right:传进来的值
    public long applyAsLong(long left, long right) {
        return left * right;
    }
}, 1);
longAccumulator.accumulate(2); //2
longAccumulator.accumulate(3); //6
System.out.println(longAccumulator.get());  //6

2、热点商品点赞计算器

热点商品点赞计算器,点赞数加加统计,不要求实时精确。比如:50个线程,每个线程100W次,总点赞数出来。分析下,点赞一次就+1,本质是多线程下的并发的i++:

class ClickNumber{
    int number = 0;
    //方式一
    public synchronized void clickBySynchronized(){
        number++;
    }
    //方式二
    AtomicLong atomicLong = new AtomicLong(0);
    public void clickByAtomicLong(){
        atomicLong.getAndIncrement();
    }
    //方式三
    LongAdder longAdder = new LongAdder();
    public void clickByLongAdder(){
        longAdder.increment();
    }
    //方式四
    LongAccumulator longAccumulator = new LongAccumulator((x,y) -> x+y,0);
    public void clickByLongAccumulator(){
        longAccumulator.accumulate(1);
    }
}

调用4种方式,借助辅助类计数器,计算5000000点赞耗时:

public class AccumulatorDemo {
    public static final int _1W = 10000;
    public static final int threadNum = 50;

    public static void main(String[] args) throws InterruptedException {
        ClickNumber clickNumber = new ClickNumber();   //共享资源类对象
        long startTime;
        long endTime;
        CountDownLatch countDownLatch1 = new CountDownLatch(threadNum);  //计数器
        CountDownLatch countDownLatch2 = new CountDownLatch(threadNum);
        CountDownLatch countDownLatch3 = new CountDownLatch(threadNum);
        CountDownLatch countDownLatch4 = new CountDownLatch(threadNum);
        //====方法一的耗时===
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= threadNum ; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * _1W; j++) {
                        clickNumber.clickBySynchronized();
                    }
                } finally {
                    //50个线程,做完一个少一个
                    countDownLatch1.countDown();
                }
            },String.valueOf(i)).start();

        }
        countDownLatch1.await();
        endTime = System.currentTimeMillis();
        System.out.println("synchronized耗时:" + (endTime - startTime) + ",当前点赞数:" + clickNumber.number);
        //====方法2的耗时===
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= threadNum ; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * _1W; j++) {
                        clickNumber.clickByAtomicLong();
                    }
                } finally {
                    //50个线程,做完一个少一个
                    countDownLatch2.countDown();
                }
            },String.valueOf(i)).start();

        }
        countDownLatch2.await();
        endTime = System.currentTimeMillis();
        System.out.println("AtomicLong耗时:" + (endTime - startTime) + ",当前点赞数:" + clickNumber.atomicLong.get());
        //====方法3的耗时===
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= threadNum ; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * _1W; j++) {
                        clickNumber.clickByLongAdder();
                    }
                } finally {
                    //50个线程,做完一个少一个
                    countDownLatch3.countDown();
                }
            },String.valueOf(i)).start();

        }
        countDownLatch3.await();
        endTime = System.currentTimeMillis();
        System.out.println("LongAdder耗时:" + (endTime - startTime) + ",当前点赞数:" + clickNumber.longAdder.sum());
        //====方法4的耗时===
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= threadNum ; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * _1W; j++) {
                        clickNumber.clickByLongAccumulator();
                    }
                } finally {
                    //50个线程,做完一个少一个
                    countDownLatch4.countDown();
                }
            },String.valueOf(i)).start();

        }
        countDownLatch4.await();
        endTime = System.currentTimeMillis();
        System.out.println("LongAccumulator耗时:" + (endTime - startTime) + ",当前点赞数:" + clickNumber.longAccumulator.get());
    }
}

运算结果:

在这里插入图片描述

结论:很大的高并发下,LongAdder的性能优于AtomicLong(减少了乐观锁的重试次数)

在这里插入图片描述

3、LongAdder高性能的原理

LongAdder --> Striped64类 --> Number类,Cell类,单元格类,是Striped64类的一个内部类。

在这里插入图片描述

Striped64类的属性解释:

在这里插入图片描述
前面的AtomicLong,N个线程同时CAS修改一个值,每次只会有一个成功,而其余N-1个线程一定失败而继续不停的自旋,N很大时,就会有大量的失败自旋。

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

sum()时会将所有Cel数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,每次并发CAS的失败线程数量就少了。

在这里插入图片描述在这里插入图片描述

LongAdder在无竞争的情况下,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,新加一个数组cells,将一个value的累加拆分进这个数组cells来分担。

在这里插入图片描述

多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到cells数组的某下标,然后对该下标所对应的值进行自增换作。当所有线程操作完毕,将数组cells的所有值和base都加起来作为最终结果。

4、源码:LongAdder-add方法

new LongAdder().increment()

各方法底层的调用关系为:

increment() --> add(1L)  --> longAccumulate()  
//最后累加的结果
--> sum()

在这里插入图片描述
从add方法开始看,方法局部变量有:

  • as:Striped64类的cells数组
  • b:Striped64类的的base值
  • v:期望值
  • m:cells数组的长度
  • a:当前线程命中的cell数组中的cell单元格对象

首次,只有一个线程的时候,base就可以完成,cell == null,casBase方法+1的操作操作成功,if条件不成立,直接跳出循环,但已经在||条件判断的时候顺带着完成了+1的操作。

public void add(long x) {
    Cell[] cs; long b, v; int m; Cell c;
    if ((cs = cells) != null || !casBase(b = base, b + x)) {
    	// true无竞争,false表示竟争激烈,多个线hash到同一个CeLL,可能要扩容
        boolean uncontended = true;
        //||下的四个条件分别为:
        //条件1:cell单元格数组为空
        //条件2:cell长度小于1,一般不会,因为到这儿说明cell不为null,而其长度2次幂起步
        //条件3:getProbe获取当前线程的哈希值,映射到cell后,cell为空,说明当前线程还没更新过cell,应初始化一个cell
        //条件4:更新当前线程所映射的cell失败,即多个线程hash到了同一个cell,说明竞争激烈,取反后到longAccumulate继续扩容
        if (cs == null || (m = cs.length - 1) < 0 ||
        	//getProbe方法返回的时线程中的threadLocalRandomProbe字段
        	//它是通过随机数生成的一个值,对于一个确定的线程,这个值固定,除非刻意修改
            (c = cs[getProbe() & m]) == null ||
            !(uncontended = c.cas(v = c.value, v + x)))
            longAccumulate(x, null, uncontended);   //Striped64中的方法扩容
    }
}

随着线程增多,CAS判断失败,false,取反后条件成立,进入if体中,uncountened=true,默认没有冲突,此时还没扩容,Cell[] as == null是成立的,进入longAccumulate(),按2次幂阔,出来两个Cell。(longAccumulate方法下面再详细展开)

在这里插入图片描述

此时,base可以+1,cell0、cell1也可以做+1,再调add,此时Cell[] as不再等于null,且length-1=1>0(2次幂) ,继续看||后面的条件,a = as[getProbe() & m],算坑位,比如算到了cell1这个单元格,此时,假设cell1中有值,为1,做个CAS,x为1,则1改为2,返回true,取反为false,跳出方法,但+1也随着条件判断完成了。

在这里插入图片描述

如上图,再并发,竞争激烈,cell0、cell1扛不住了也,cell上cas失败,uncontended为false,取反,true,再进入longAccumulate()扩容,2个变4个。

总结:

  • 最初无竞争时只更新base;
  • 如果更新base失败后,首次新建一个Cell[ ]数组
  • 当多个线程竞争同一个Cell比较激烈时,可能就要对Cell[ ]扩容

代码亮点:调用方法做为判断条件,最终的效果就是活儿干了(数据改变了),条件也做了判断了。借鉴!

简略版图解:

在这里插入图片描述

5、源码:LongAdder-longAccumulate方法

Striped64类中的一些属性和方法:getProbe方法,获取线程的hash值,这个值用于判断去cell数组的哪个槽位中去。

在这里插入图片描述

//getProbe方法返回的时线程中的threadLocalRandomProbe字段
static final int getProbe(){
	return UNSAFE.getInt(Thread.currentThread(),PROBE);
}

longAccumulate()方法的入参:

  • long x :需要做雷加的值,increment调用下,一般默认都是+1
  • LongBinaryOperator fn :默认传递的是null
  • wasUncontended:竞争标识,如果是false则代表有竞争,只有cells初始化之后,并且当前线程CAS竞争修改失败,才会是false

longAccumulate方法开头处理下线程的probe值:

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竞争状态为true,表示无竞争
            h = getProbe();
            //重新计算了当前线程的hash后认为此次不算是一次竞争,都未初始化,背定还不存在竟争激烈,wasUncontended竞争状态为true
            wasUncontended = true;
        }
//......

然后longAccumulate方法源码大体结构为:首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为三个分支:

  • CASE1: Cell[ ] 数组已经初始化

  • CASE2:CelI[ ] 数组未初始化(首次新建)

  • CASE3:Cell[ ] 数组正在初始化中

在这里插入图片描述

先看Case2:未初始化过Cell[ ] 数组,尝试占有锁并首次初始化cells数组

cellsBusy:初始化cells或者扩容cells需要获取锁,0表示无锁状态,1表示其他线程已经持有了锁。为0时,抢到锁,&&后面casCellsBusy改为1,初始化创建Cell[2]后,finally中cellsBusy改回0,注意下面有点双重检锁的味道。

在这里插入图片描述

如果上面条件都执行成功就会执行数组的初始化及赋值操作, Cell[] rs = new Cell[2]表示数组的长度为2,rs[h & 1]= new Cell(x) 即创建一个新的Cell元素,value是累加的值x,默认为1。h & 1类似于之前HashMap常用到的计算散列桶index的算法,通常都是hash & (table.len - 1),同hashmap一个意恩。

再看Case3:上面竞争很激烈,else兜底的,多个线程尝试CAS修改失败的线程会走到这个分支

该分支直接操作base,将值累加到base

在这里插入图片描述

最后看Case1:Cell数组不再为空且可能存在Cell数组扩容,多个线程同时命中一个cell的竞争。此个If分支又分为6中if情况:

在这里插入图片描述

上面代码判断当前线程hash后指向的数据位置元素是否为空,如果为空则将Cell数据放入数组中,跳出循环。如果不空则继续循环。

在这里插入图片描述

wasUncontended表示cells初始化后,当前线程竞争修改失败wasUncontended =false,这里只是重新设置了这个值为true,紧接着执行Striped64类的advanceProbe(h)方法重置当前线程的hash,重新循环,重新再竞争一次。

在这里插入图片描述说明当前线程对应的数组中有了数据,也重置过hash值,这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS成功则直接break跳出循环。

在这里插入图片描述

如果n大于CPU最大数量,不可扩容并通过下面的h=advanceProbe(h)方法修改线程的probe再重新尝试。

在这里插入图片描述

如果扩容意向collide是false则修改它为true,然后重新算当前线程的hash值继续循环,如果当前数组的长度已经大于了CPU的核数,就会再次设置扩容意向collide=false (见上一步)

在这里插入图片描述

总结:

在这里插入图片描述
在这里插入图片描述

6、源码:LongAdder-sum方法

在这里插入图片描述
sum()会将斯有Cell数组中的value和base累加作为返回值。核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。

在这里插入图片描述

为啥并发情况下sum的值不精确?

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

首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。其次,这里对cell的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果。

7、AtomicLong和LongAdder的对比

AtomicLong:

  • 通过CAS+自旋实现
  • 线程安全,可允许一些性能损耗,要求高精度时选AtomicLong
  • 保证精度,但以性能为代价
  • AtomicLong是多个线程针对单个热点值value进行原子操作
  • 缺点是高并发下,性能急剧下降,且AtomicLong的自旋同时也是瓶颈:因为N个线程同时CAS一个值,只有一个线程成功,其余N-1个线程要不断自旋

LongAdder:

  • 通过CAS+Base +Cell数组分散热点来实现
  • 当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
  • 保证性能,但以精度为代价
  • LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作
  • 缺陷是:sum求和后,还有计算线程修改结果的话,最后返回的结果不够准确

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI+HR黑科技秘笈:AI赋能人力资本智能化变革

今天分享的是AI系列深度研究报告&#xff1a;《AIHR黑科技秘笈&#xff1a;AI赋能人力资本智能化变革》。 &#xff08;报告出品方&#xff1a;Ifchange&#xff09; 报告共计&#xff1a;98页 让AI技术提升人岗匹配效果&#xff0c;我们做了这些探索 AI 技术实现人岗匹配&a…

DTCC2023大会-DBdoctor-基于eBPF观测数据库-附所有PPT下载链接

DTCC2023大会-DBdoctor-基于eBPF观测数据库-附所有PPT下载链接 8月16日—18日,第14届中国数据库技术大会(DTCC-2023)在北京国际会议中心举行。聚好看在大会上首次发布基于eBPF观测数据库性能的产品DBdoctor&#xff0c;受到了业界广泛的关注。近期几位业内同仁过来要大会的PPT…

滑动窗口练习(三)— 加油站问题

题目 测试链接 在一条环路上有 n 个加油站&#xff0c;其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车&#xff0c;从第 i 个加油站开往第 i1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发&#xff0c;开始时油箱为空。 给定两个整数数组…

六:Day03_Mybatis-Plus

一、介绍 MyBatis-Plus&#xff08;简称 MP&#xff0c;是由baomidou(苞米豆)组织开源的&#xff09;是一个基于 MyBatis 的增强工具&#xff0c;它对 Mybatis 的基础功能进行了增强&#xff0c;但未做任何改变&#xff0c;Mybatis-Plus 其实可以看作是对 Mybatis 的再一次封装…

Linux上使用独立显卡Tesla T4(测试视频压缩)

背景 将视频处理程序单独部署至K8S之外&#xff0c;使用独立GPU显卡的一台服务器上。 需事先对GPU性能做简单测试。 已通过zabbix对Linux进行了系统资源监控。 已通过PrometheusGrafana对显卡Tesla T4做了性能监控。 逐步补充&#xff0c;稍等 2023年12月6日 操作 查看当前…

案例005:基于小程序的电子点菜系统开发设计与实现

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

【LSM tree 】Log-structured merge-tree 一种分层、有序、面向磁盘的数据结构

文章目录 前言基本原理读写流程写流程读流程 写放大、读放大和空间放大优化 前言 LSM Tree 全称是Log-structured merge-tree, 是一种分层&#xff0c;有序&#xff0c;面向磁盘的数据结构。其核心原理是磁盘批量顺序写比随机写性能高很多&#xff0c;可以通过围绕这一原理进行…

C语言--实现一个函数把一个整数转为它对应的十六进制的字符串

一.题目描述 实现一个函数把一个整数转为它对应的十六进制的字符串。 比如&#xff1a;输入数字1234 输出&#xff1a;4D2 二.思路分析 用一个sprintf函数可以解决问题&#xff0c;输出相对应的字符串 要注意的问题就是&#xff1a;函数结束后要继续使用的内存&#xff08;比如…

【MATLAB】基于EMD分解的信号去噪算法(基础版)

代码操作 【MATLAB】基于EMD分解的信号去噪算法&#xff08;基础版&#xff09; 代码的主要内容 基于EMD&#xff08;经验模态分解&#xff09;的信号去噪算法通常可以结合相关系数、信号的熵值或者方差贡献率来完成去噪处理。这些指标可以用于确定阈值&#xff0c;从而对信号…

CentOS 7 离线安装达梦数据库8.0

前期准备工作 确认操作系统的版本和数据库的版本是否一致 ## 查看系统版本&#xff1a;cat /etc/redhat-release CentOS Linux release 7.5.1804 (Core)关闭防火墙和Selinux # 查看selinux是不是disabled / enforce cat /etc/selinux/config## 查看防火墙状态 firewall-cmd …

使用医学数据集MIMIC,常见的问题记录

目录 MIMIC数据库安装及数据导入教程1.postgresql安装第一步&#xff1a;error running考虑到是不是不同的sql的冲突从报错信息出发重启之后可以安装了 2.打开navicate153.7z 不是内部或外部命令&#xff0c;也不是可运行的程序4.在postgreSQL中输入**\i xxx**命令后遇到提示pe…

Leetcode—231.2的幂【简单】

2023每日刷题&#xff08;五十四&#xff09; Leetcode—231.2的幂 实现代码 class Solution { public:bool isPowerOfTwo(int n) {if(n < 0) {return false;}long long ans 1;while(ans < n) {ans * 2;}if(ans n) {return true;}return false;} };运行结果 之后我会…

C++ 哈希表实现

目录 前言 一、什么是哈希表 二、直接定值法 三、开放定值法&#xff08;闭散列&#xff09; 1.开放定制法定义 2.开放定制法实现 2.1类的参数 2.2类的构造 2.3查找实现 2.4插入实现 2.5删除实现 2.6string做key 四、哈希桶&#xff08;开散列&#xff09; 1.开散…

Java基础-java.util.Random生成指定区间的随机数

目录 1. 公式2. 导包3. 编写代码4. 输出运行结果 1. 公式 生成[a, b]区间的随机数&#xff1a; random.nextInt((b - a) 1) a 2. 导包 import java.util.Random;3. 编写代码 public class random_demo {public static void main(String[] args) {/** 随机数Random* 需求&am…

【力扣】移除链表元素203

目录 1.前言2. 题目描述3. 题目分析3.1 不带哨兵位3.2 带哨兵位 4. 附代码4.1 不带哨兵位4.2 带哨兵位 1.前言 这里开始介绍从网上一些刷题网站上的题目&#xff0c;在这里做一些分享&#xff0c;和学习记录。 先来介绍一些力扣的OJ题目。 这里的OJ就是我们不需要写主函数&…

【python交互界面】实现动态观察图像在给定HSV范围的区域显示

HSV颜色空间 与RGB颜色空间相比&#xff0c;HSV颜色空间更适合进行颜色分析和提取特定颜色的目标。在HSV空间中&#xff0c;颜色信息被分布在不同的通道上&#xff0c;使我们能够更准确地定义颜色的范围&#xff0c;并使用阈值操作轻松地分离出我们感兴趣的区域部分。 HSV三个通…

魔改ESXI 8.0驱动,支持Intel I219V (22)网卡(8086:0DC8)(无需改网卡ROM)

艰难的安装 最近用铭瑄H610-itx攒了一台nas&#xff0c;想着兼容性高一些专门买了H610的双网卡版本&#xff0c;一张是螃蟹的8125BG&#xff0c;一张是intel的i219V&#xff0c;还以为esxi总不能一个网卡都认不出来吧&#xff0c;然而装好机启动esxi8.0的安装程序一看&#xf…

智能优化算法应用:基于灰狼算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于灰狼算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于灰狼算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.灰狼算法4.实验参数设定5.算法结果6.参考文献7.MA…

class_2:Java概念 java se ee me jdk jre jvm

一、什么是Java&#xff1f; Java是一门面向对象的编程语言&#xff0c;不仅吸收了C语言的各种优点&#xff0c;还摒弃了C里难以理解的多继承、指针等概念&#xff0c;因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表&#xff0c;极好地…

常见的消息中间件看这一篇就够了

浅谈消息队列及常见的消息中间件 前言 消息队列 已经逐渐成为企业应用系统 内部通信 的核心手段。它具有 低耦合、可靠投递、广播、流量控制、最终一致性 等一系列功能。 当前使用较多的 消息队列 有 RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMQ 等&#xff0c;而部…