【并发编程】原子累加器

news2024/11/18 23:37:02

       📝个人主页:五敷有你      
 🔥系列专栏:并发编程
⛺️稳重求进,晒太阳

JDK8之后有专门做累加的类,效率比自己做快数倍以上

累加器性能比较

参数是方法

  • // supplier 提供者 无中生有 ()->结果
  • // function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
  • // consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->void
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());
}

原子累加器 花费116ms, 自己写花费 938ms 

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

源码之LongAdder

LongAdder 是并发大师 @author Doug Lea 的作品,设计精巧

LongAdder类有几个关键域

// 累加单元数组, 懒惰初始化
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);
    }
}

 测试

LockCas lock = new LockCas();
new Thread(() -> {
    System.out.println("begin...");
    lock.lock();
    try {
        System.out.println("lock...");
        sleep(1000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        lock.unlock();
    }
}).start();
new Thread(() -> {
    System.out.println("begin...");
    lock.lock();
    try {
        System.out.println("lock...");
    } finally {
        lock.unlock();
    }
}).start();

输出

原理之伪共享

其中 Cell 即为累加单元

得从缓存说起

缓存与内存的速度比较

因为 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 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding(填充),从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

累加主要调用下面的方法

  public void add(long x) {
        // as 为累加单元数组
        // b 为基础值
        // x 为累加值
        Cell[] as; long b, v; int m; Cell a;
        // 进入 if 的两个条件
        // 1. as 有值, 表示已经发生过竞争, 进入 if
        // 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            // uncontended 表示 cell 没有竞争
            boolean uncontended = true;
            if (
                // as 还没有创建
                    as == null || (m = as.length - 1) < 0 ||
                            // 当前线程对应的 cell 还没有
                            (a = as[getProbe() & m]) == null ||
                            // cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell )
                            !(uncontended = a.cas(v = a.value, v + x))
            ) {
                // 进入 cell 数组创建、cell 创建的流程
                longAccumulate(x, null, uncontended);
            }
        }
    }

add 流程图

final void longAccumulate(long x, LongBinaryOperator fn,
                          boolean wasUncontended) {
    int h;
    // 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell
    if ((h = getProbe()) == 0) {
        // 初始化 probe
        ThreadLocalRandom.current();
        // h 对应新的 probe 值, 用来对应 cell
        h = getProbe();
        wasUncontended = true;
    }
    // collide 为 true 表示需要扩容
    boolean collide = false;
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        // 已经有了 cells
        if ((as = cells) != null && (n = as.length) > 0) {
            // 还没有 cell

            if ((a = as[(n - 1) & h]) == null) {
                // 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x
                // 成功则 break, 否则继续 continue 循环
            }
            // 有竞争, 改变线程对应的 cell 来重试 cas
            else if (!wasUncontended)
                wasUncontended = true;
                // cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null
            else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                break;
                // 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas
            else if (n >= NCPU || cells != as)
                collide = false;
                // 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
            else if (!collide)
                collide = true;
                // 加锁
            else if (cellsBusy == 0 && casCellsBusy()) {
                // 加锁成功, 扩容
                continue;
            }
            // 改变线程对应的 cell
            h = advanceProbe(h);
        }
        // 还没有 cells, 尝试给 cellsBusy 加锁
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            // 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell
            // 成功则 break;
        }
        // 上两种情况失败, 尝试给 base 累加
        else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
            break;
    }

longAccumulate 流程图

每个线程刚进入 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.coloradmin.cn/o/1435111.html

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

相关文章

3.0 Zookeeper linux 服务端集群搭建步骤

本章节将示范三台 zookeeper 服务端集群搭建步骤。 所需准备工作&#xff0c;创建三台虚拟机环境并安装好 java 开发工具包 JDK&#xff0c;可以使用 VM 或者 vagrantvirtualbox 搭建 centos/ubuntu 环境&#xff0c;本案例基于宿主机 windows10 系统同时使用 vagrantvirtualb…

负重20kg复合翼垂直起降无人机应用,复合翼无人机技术分析

主要任务应用 1.管线巡查 挂载可见光/红外二合一光电载荷和小型SAR设备&#xff0c;对既定线路进行昼夜巡视侦察&#xff0c;利用图像实时传回指挥控制中心&#xff0c;可用于石油管路、电力线路、舰艇航线及周围态势感知&#xff0c;利于依据现场实情进行战略决策和指令传达…

文心一言 VS 讯飞星火 VS chatgpt (195)-- 算法导论14.3 3题

三、用go语言&#xff0c;请给出一个有效的算法&#xff0c;对一个给定的区间i&#xff0c;返回一个与i重叠且具有最小低端点的区间;或者当这样的区间不存在时返回T.nil。 文心一言&#xff0c;去掉math&#xff0c;代码正常运行&#xff1a; 在Go语言中&#xff0c;你可以使…

栈--数据结构

初始化栈、入栈、出栈 栈&#xff1a;只允许在一端进行插入或删除操作的线性表 栈顶&#xff08;Top&#xff09; 代码实战步骤依次是初始化栈&#xff0c;判断栈是否为空&#xff0c;压栈&#xff0c;获取栈顶元素&#xff0c;弹栈。 代码 #include <stdio.h>#define …

Linux系统c/c++开发环境配置

安装LLVM全家桶及CMAKE 输入以下命令&#xff0c;安装clang&#xff0c;clangd&#xff0c;lldb及cmake。 sudo apt install clang clangd lldb cmake yukeyangDESKTOP-QFK2F47:~/myfiles/test$ sudo apt install clang clangd lldb cmake [sudo] password for yukeyang: Re…

re:从0开始的CSS学习之路 1. CSS语法规则

0. 写在前面 现在大模型卷的飞起&#xff0c;感觉做页面的活可能以后就不需要人来做了&#xff0c;不知道现在还有没有学前端的必要。。。 1. HTML和CSS结合的三种方式 在HTML中&#xff0c;我们强调HTML并不关心显示样式&#xff0c;样式是CSS的工作&#xff0c;现在就轮到C…

如何在Linux中安装新版的Python软件

一、引言 Python是目前世界上最为流行的编程语言&#xff0c;其在人工智能领域表现尤为出色。通常&#xff0c;我们为了测试github上面的一些项目&#xff0c;比如&#xff1a;chat-on-wechat&#xff0c; 我们就可以在vps上的Linux系统中安装Python&#xff0c;从而实现各种人…

聚观早报 | iOS 17.4正式版将上线;魅族21 Pro或下月发布

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 2月5日消息 iOS 17.4正式版将上线 魅族21 Pro或下月发布 小米MIX Flip细节曝光 OPPO Find X7 Ultra卫星通信版 …

相机图像质量研究(3)图像质量测试介绍

系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结&#xff1a;光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结&#xff1a;光学结构对成…

基于BiLSTM-CRF模型的分词、词性标注、信息抽取任务的详解,侧重模型推导细化以及LAC分词实践

基于BiLSTM-CRF模型的分词、词性标注、信息抽取任务的详解,侧重模型推导细化以及LAC分词实践 1.GRU简介 GRU(Gate Recurrent Unit)门控循环单元,是[循环神经网络](RNN)的变种种,与 LSTM 类似通过门控单元解决 RNN 中不能长期记忆和反向传播中的梯度等问题。与 LSTM 相…

Android用setRectToRect实现Bitmap基于Matrix矩阵scale缩放RectF动画,Kotlin(一)

Android用setRectToRect实现Bitmap基于Matrix矩阵scale缩放RectF动画&#xff0c;Kotlin&#xff08;一&#xff09; 基于Matrix&#xff0c;控制Bitmap的setRectToRect的目标RectF的宽高。从很小的宽高开始&#xff0c;不断迭代增加setRectToRect的目标RectF的宽高&#xff0c…

python进行批量搜索匹配替换文本文字的matlab操作实例

在进行一些数据处理时&#xff0c;可能需要抓取原文中的一些内容&#xff0c;批量替换原文另外的一些内容&#xff0c;而且事先还需要一步搜索匹配的步骤。 举个例子&#xff0c;如下matlab输出的txt文件&#xff0c;原文件有几万行数据&#xff0c;这里只摘取3行对应的 文件文…

react 之 react.forwardRef

react.forwardRef使用ref暴露DOM节点给父组件 1.使用场景 import { forwardRef, useRef } from "react"// 子组件 // function Son () { // return <input type"text" /> // }const Son forwardRef((props, ref) > {return <input type&qu…

Golang与Erlang有什么差异

Golang和Erlang是两种备受关注的编程语言&#xff0c;它们各自具有独特的特点和优势。下面我将简单的探讨一下Golang和Erlang之间的差异&#xff0c;并且分析它们在并发模型、运行环境、函数式编程和领域特性等多个方面的不同之处。 并发模型 Golang使用goroutines和channels…

分布式延时消息的另外一种选择 Redisson (推荐使用)

前言 目录 前言 基本使用 内部数据结构介绍 基本流程 发送延时消息 获取延时消息 初始化延时队列 总结 因为工作中需要用到分布式的延时队列&#xff0c;调研了一段时间&#xff0c;选择使用 Redisson DelayedQueue&#xff0c;为了搞清楚内部运行流程&#xff0c;特记…

何时用‘x-->?‘将其代入lim中?极限的化简

【何时用‘x-->?将其代入lim中】 【极限的化简】 与极限解法区别&#xff1f; 方法有哪些&#xff1f; 什么条件下可以用&#xff1f; 怎么用&#xff1f;

Bootstrap5 图片轮播

Bootstrap5 轮播样式表使用的是CDN资源 <title>亚丁号</title><!-- 自定义样式表 --><link href"static/front/css/front.css" rel"stylesheet" /><!-- 新 Bootstrap5 核心 CSS 文件 --><link rel"stylesheet"…

大型语言模型(LLM)的优势、劣势和风险

最近关于大型语言模型的奇迹&#xff08;&#xff09;已经说了很多LLMs。这些荣誉大多是当之无愧的。让 ChatGPT 描述广义相对论&#xff0c;你会得到一个非常好&#xff08;且准确&#xff09;的答案。然而&#xff0c;归根结底&#xff0c;ChatGPT 仍然是一个盲目执行其指令集…

CodeFuse-VLM 开源,支持多模态多任务预训练/微调

CodeFuse-MFT-VLM 项目地址&#xff1a;https://github.com/codefuse-ai/CodeFuse-MFT-VLM CodeFuse-VLM-14B 模型地址&#xff1a;CodeFuse-VLM-14B CodeFuse-VLM框架简介 随着huggingface开源社区的不断更新&#xff0c;会有更多的vision encoder 和 LLM 底座发布&#x…

政安晨:机器学习快速入门(一){基于Python与Pandas}

对于刚接触ML&#xff08;机器学习&#xff09;的小伙伴来说&#xff0c;通过几篇文章能够快速登堂入室是非常及时且有用的&#xff0c;作者政安晨力求让小伙伴们&#xff0c;几篇文章内就可以达到这个目标&#xff0c;咱们开始&#xff01; 机器学习简介 咱们先看一下Pandas&…