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

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

文章目录

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

累加器性能比较

java">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

java">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:22

20000000 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 类有几个关键域

java">// 累加单元数组, 懒惰初始化(多线程去累加的时候,每个线程用各自的一个Cell累加单元来累加,可以减少从试,从而提高性能)
transient volatile Cell[] cells;

// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;

// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;

cas 锁

java">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)

测试

java">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 即为累加单元

java">// 防止缓存行伪共享
@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源码

在这里插入图片描述

累加主要调用下面的方法

java">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 流程图

在这里插入图片描述

java">final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (;;) { // 相当于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 table
                    if (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 流程图

在这里插入图片描述

java">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 Cell
                        Cell r = new Cell(x);   // Optimistically create
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] 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;

在这里插入图片描述

java">if ((as = cells) != null && (n = as.length) > 0) {
                if ((a = as[(n - 1) & h]) == null) {
                    ...
                }
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else 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 stale
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == as) {      // Expand table unless stale
                            Cell[] 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 方法

java">	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.niftyadmin.cn/n/5148278.html

相关文章

家用洗地机哪个牌子质量最好?家用洗地机推荐

洗地机也就是集吸尘器&#xff0c;拖地&#xff0c;洗地&#xff0c;功能于一体的家电&#xff0c;无论干湿垃圾都能清理的干干净净&#xff0c;而且还不用弯腰&#xff0c;有的只用换个头&#xff0c;就从拖地变成了吸尘器和除螨仪简直就是清洁家里卫生的打扫神器啦!那么面对市…

解决Visual Studio Code 控制台中文乱码问题

C和CPP运行编码指定 "code-runner.executorMap": {"c": "cd $dir && gcc -fexec-charsetGBK $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt","cpp": "cd $dir && g -fexec-charsetGBK $…

电子凭证会计数据标准试点深化后,企业管理的关键点在于什么?

为了加快建设数字中国、发展数字经济发展&#xff0c;并推动经济社会绿色化、低碳化发展&#xff0c;政府部门一直大力推动企业的数字化转型。 企业的经营活动也越来越活跃。企业在经营中产生了大量的票据&#xff0c;由于电子凭证分属不同的部门管理&#xff0c;数据不兼容&am…

苹果IOS系统webglcontextlost问题-解决方案

问题描述 在IOS手机 解码视频流的时候&#xff0c;第一次可以正常播放&#xff0c;但只要IOS手机熄屏&#xff0c;再重新唤醒&#xff0c;就会一直播放失败&#xff0c;无论换哪个浏览器都不行。安卓手机则一切正常。 经过排查&#xff0c;发现 IOS手机 的浏览器会无故 webGL…

黑马程序员项目-黑马点评

黑马点评1 短信登录 基于Session实现登录流程 发送验证码&#xff1a; 用户在提交手机号后&#xff0c;会校验手机号是否合法&#xff0c;如果不合法&#xff0c;则要求用户重新输入手机号 如果手机号合法&#xff0c;后台此时生成对应的验证码&#xff0c;同时将验证码进行…

steam搬砖项目好做吗,这几点你一定必须要知道

如何评价现在的csgo市场&#xff1f; 下面详细几点概述一下&#xff1a; 1.并非所有的饰品都能够带来收益&#xff0c;因此在steam搬砖项目中&#xff0c;需要通过查看走势图来选择潜在赚钱的饰品。这可能需要花费两三个小时的时间&#xff0c;或者浏览数百个饰品&#xff0c;…

LeeCode算法题

1.两数之和 哈希表 2.字母异位词分组 哈希表&#xff0c;键是排序的字母组&#xff0c;值是列表 3.最长连续序列 遍历一次存哈希表先&#xff0c;然后找开头&#xff0c;找到开头开始1在哈希表里找 4.移动零 使用双指针&#xff0c;交换的做法

【ZMQ】ZMQ/ZeroMQ简介、三种消息模式demo程序

ZMQ/ZeroMQ简介、三种消息模式demo程序 一、什么是ZMQ二、ZMQ的特点三、Demo程序代码3.1 发布-订阅模式&#xff08;P/S&#xff09;demo3.2 请求-应答模式&#xff08;REQ/RES&#xff09;demo3.3 推拉模式&#xff08;P/P&#xff09;demo 一、什么是ZMQ ZeroMQ&#xff08;…