并发编程(二十八) - LongAdder原理

一、LongAdder原理

1. LongAdder架构

在这里插入图片描述

LongAdder 是 Striped64 的子类,Striped64 是 Number 子类

public class LongAdder extends Striped64 {}
abstract class Striped64 extends Number {}

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值

==============================================================
abstract class Striped64 extends Number {
	
	// CPU数量,即Cells数组的最大长度
	static final int NCPU = Runtime.getRuntime().availableProcessors();
	
	// 存放Cell的hash表,大小为2的幂
	// 这里的Cell是Striped64的静态内部类,懒惰初始化
	transient volatile Cell[] cells;
	
	/*
	1.在开始没有竞争的情况下,将累加值累加到base;
	2.在cells初始化的过程中,cells处于不可用的状态,这时候也会尝试将通过cas操作值累加到base
	*/
	transient volatile long base;
	
	/*
	cellsBusy,它有两个值0或1,它的作用是当要修改cells数组时加锁,
	防止多线程同时修改cells数组(也称cells表),0为无锁,1位加锁,加锁的状况有三种:
	(1)cells数组初始化的时候;
    (2)cells数组扩容的时候;
    (3)如果cells数组中某个元素为null,给这个位置创建新的Cell对象的时候;
	*/
	transient volatile int cellsBusy;
}

3. LongAdder为什么快?

  • LongAdder基本思路就是分散热点,将value分散到一个Cell数组中,不同线程会命中数组的不同槽位中,各个线程只对自己槽位的value进行CAS操作,这样热点就被分散了,冲突概率小了
  • sum()会将所有Cell数组中的value和base累加作为返回值,核心思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点
  • 内部有一个base+一个Cell[ ]数组
    • base变量:低并发,直接累加到该变量上
    • Cell[ ]数组:高并发,累加到各个线程自己的槽Cell[i]中

在这里插入图片描述

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

4. LongAdder之缓存行伪共享

(1)Striped64中静态内部类Cell源码

abstract class Striped64 extends Number {
	// 防止缓存行伪共享 注解
	@sun.misc.Contended
	static final class Cell { // Cell 即为累加单元
		 // 保存累加结果
		 volatile long value;
		 // 构造方法为value赋初始值
		 Cell(long x) { value = x; }
		 
		 // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
		 final boolean cas(long prev, long next) {
		 	return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
		 }
		 // 省略其他代码
	}
}

(2)CPU缓存结构

在这里插入图片描述

(3)缓存与内存的速度比较

从 cpu 到大约需要的时钟周期
寄存器1 cycle (4GHz 的 CPU 约为0.25ns)
L13~4 cycle
L210~20 cycle
L340~45 cycle
内存120~240 cycle
  • 由于CPU的速度远远大于内存速度,所以需要靠预读数据至缓存来提升效率
  • CPU Cache分成了三个级别:L1,L2,L3。级别越小越接近CPU, 所以速度越快, 但是容量越小
  • CPU获取数据会依次从L1,L2,L3中查找,如果都找不到则会直接从内存查找
  • 而缓存是以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)
  • 缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
  • CPU 需要保证数据的一致性,所以如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效

例如:线程1将数据 x 从内存读到缓存行,线程2也将数据 x 从内存读到缓存行,当线程1将x修改为y,并设置到内存中,则线程2的整个缓存行数据需要失效,重新从内存中读取y

分析

在这里插入图片描述

static final class Cell { // 对象头16字节
	volatile long value; // long 8字节
 }

因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 对象为 24 字节(16 字节的对象头和 8 字节的 long 类型的 value),缓存行一般存64字节,所以可以存下 2 个 Cell 对象(Cell[0]、Cell[1])。

这样问题来了,假设:

  • Core-0 要修改 Cell[0]
  • Core-1 要修改 Cell[1]
  1. 无论谁修改成功,都会导致对方 Core 的缓存行失效
  • 比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 只累加Cell[0],累加后Core-0 中 Cell[0]=6001, Cell[1]=8000
  • 虽然Core-0 中 Cell[1]=8000没累加,但是 Cell[0]、 Cell[1]是在同一个缓存行里,所以Core-1的缓存行需要整行失效,重新从头读取Cell[0]=6001,Cell[1]=8000,这就出现了伪共享问题,从而影响效率
  1. @sun.misc.Contended 就是用来解决伪共享,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding(空白,用来占位),从而让 CPU 将对象预读至缓存时,占用不同的缓存行(例如:让Cell[0]、Cell[1]占用不同的缓存行,占用Cell[0]变了不会影响Cell[1]),这样,不会造成对方缓存行的失效

伪共享(False Sharing)是指多个线程同时读写同一个缓存行的不同变量时,导致 CPU缓存失效。尽管这些变量之间没有任何关系,但由于在主内存中邻近,存在于同一个缓存行之中,它们的相互覆盖会导致频繁的缓存未命中,引发性能下降。

5. LongAdder 源码

(1)add方法

对外调用

LongAdder adder = new LongAdder();
adder.increment();

内部调用 add 方法源码

long x: 累加值
Cell[] as: 累加单元数组cells的引用
long b: 获取的base值
long v: 期望值(当前cell存储的值)
int m: cells数组的长度
Cell a: 当前线程命中的cell单元格
boolean uncontended: 表示cell是否没有竞争
========================================================================
public class LongAdder extends Striped64 implements Serializable {
	
	public void increment() {
        add(1L);
    }
	
	public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        // 进入 if 的两个条件
 		// 条件1:as 有值, 表示已经发生过竞争, 进入 if
 		// 条件2:cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
 		// 由于 cells 是懒惰初始化,刚开始是null,会先进行casBase操作
        if ((as = cells) != null || !casBase(b = base, b + x)) {      		
            boolean uncontended = true;
            /*
			条件1: cells为空,说明正在出现竞争,上面是从条件2过来的,说明!casBase(b = base, b + x))=true
				  会通过调用longAccumulate(x, null, uncontended)新建一个数组,默认长度是2
			条件2: 默认会新建一个数组长度为2的数组,m = as.length - 1 < 0 应该不会出现
			条件3: 当前线程所在的cell为空,说明当前线程还没有更新过cell,uncontended为true,应初始化一个cell
			条件4: 更新当前线程所在的cell失败,说明竞争很激烈,多个线程hash到同一个Cell,uncontended为false,应扩容
			**/
            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); // 创建cells数组的流程
        }
    }
}

add 各种场景:

  1. cells = null,casBase(b = base, b + x)累加失败,执行 longAccumulate(x, null, uncontended);
  2. cells != null,as[getProbe() & m] == null 表示当前线程还没创建cell,执行 longAccumulate(x, null, uncontended);
  3. cells != null,如果as[getProbe() & m] != null 表示当前线程创建了cell,执行累加单元cell的cas操作:a.cas(v = a.value, v + x),失败则执行 longAccumulate(x, null, uncontended);在这里插入图片描述

小结:

  1. 最开始只更新base
  2. 更新失败,则新创建一个Cell[]数组
  3. 多个线程竞争到同一个Cell时,进行Cell[]数组扩容

(2)继承Striped64中的 longAccumulate方法

前置知识1:getProbe()方法获取线程hash值
// Striped64中的longAccumulate方法
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
	// 存储线程的hash值,有了hash值就可以知道当前线程进入哪个槽位
	int h;
	// 如果getProbe()方法返回0,说明随机数未初始化,需要初始化后,线程才能进入对应槽位
	if ((h = getProbe()) == 0) {
		// 使用ThreadLocalRandom为当前线程重新计算一个hash值,强制初始化
		ThreadLocalRandom.current(); 
		// 重新获取hash值
		h = getProbe();
		// 重新获取hash值后,认为此次不算是一次竞争,都未初始化,肯定还不存在竞争激烈,所以wasUncontended竞争状态为true
		wasUncontended = true;
}

// Striped64中的getProbe方法,得到当前线程的hash值PROBE
static final int getProbe() {
     return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
前置知识2:longAccumulate方法总体逻辑

在这里插入图片描述

注意:执行顺序是从CASE2开始,CASE3是兜底方案,当大量线程同时进行CASE2时,只有一个线程能进入CASE2,失败的则进入CASE3
① 第一个线程创建cells 并初始化 一个累加单元cell

long x: 累加值
LongBinaryOperator fn: 默认null
boolean wasUncontended: 是否没竞争,false表示有竞争;只有cells初始化之后,并且CAS累加值失败,才为false

long base: 类似于AtomicLong中全局的value值。在没有竞争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
boolean collide: 表示扩容意向,false一定不会扩容,true可能会扩容
cellsBusy: 初始化cells或者扩容cells需要获取锁,0表示无锁状态,1表示其他线程已经持有了锁
casCellsBusy: 通过CAS操作修改cellsBusy的值, CAS成功代表获取锁,返回true
NCPU: 当前计算机CPU数量,Cell数组扩容时会使用到
getProbe(): 获取当前线程的hash值
advanceProbe(): 重置当前线程的hash值

=================================================================================================

final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
        int h;
        // 获取线程hash值,前面已经分析过,将代码隐藏
        if ((h = getProbe()) == 0) {...}
        boolean collide = false;               
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            // 后面分析第一个if,先将代码隐藏
            if ((as = cells) != null && (n = as.length) > 0) {...}
            // 创建cells的场景
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) { // 再次检查cells引用是否改变,双重检测是为了避免并发场景下,重复创建cells
                        Cell[] rs = new Cell[2]; // 创建长度为2的cell数组
                        rs[h & 1] = new Cell(x); // 将累加值x随机存放到cell数组对应的索引下标位置
                        cells = rs; // 再将创建的cell数组引用赋值给cells
                        init = true;
                    }
                } finally {
                    cellsBusy = 0; // 创建完cell数组后,解锁
                }
                if (init)
                    break; // 退出循环
            }
            // 尝试cas累加
            else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }
  1. 进入for循环,先进入第一个else if,cellsBusy == 0 还未加锁,cells == as 表示cells是当前线程的引用,其他线程还未创建cells,casCellsBusy()方法通过cas尝试将cellsBusy改为1表示加锁,加锁成功,其他线程就不会来干扰cells的创建
  2. casCellsBusy()方法通过cas尝试加锁失败后,进入第二个else if,casBase()方法尝试cas累加,成功则返回,失败则重新进入for循环
    在这里插入图片描述
② 在第一个线程基础上,第二个线程赋值给cell中的空槽位
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {...}
        boolean collide = false;             
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            // 分析第一个if
            if ((as = cells) != null && (n = as.length) > 0) { // cells已经被创建
                if ((a = as[(n - 1) & h]) == null) {// cells虽然创建了,但是其中累加单元cell[0]或cell[1]还没被其他线程使用,则进入该if
                    if (cellsBusy == 0) { // 其他线程没使用,自然也就没加锁    
                        Cell r = new Cell(x); // 创建累加单元,还没赋值到cells数组中 
                        // 该if是将创建的累加单元,设置到cells数组的空位置(cell[0]或cell[1]位置)
                        // 双重检测cellsBusy == 0,避免并发场景时重复赋值
                        if (cellsBusy == 0 && casCellsBusy()) {// 进入该if之前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
        }
    }

在这里插入图片描述

③ 线程获取到了已经有值的累加单元cell,并在该cell上尝试进行累加
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {...}
        boolean collide = false;               
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            if ((as = cells) != null && (n = as.length) > 0) {
            	// 已经分析过
            	if ((a = as[(n - 1) & h]) == null) {...}
                else if (!wasUncontended)       // wasUncontended为false说明在同一个槽位竞争失败
                    wasUncontended = true;      // 跳到下面h = advanceProbe(h)位置,重新hash换一个槽位累加
                
                // 尝试对已经有值的累加单元cell进行累加,成功则退出
                else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                    break;
                
                // 累加失败,判断是否超过CPU上限    
                else if (n >= NCPU || cells != as)
                	// 这个设计很巧妙
                	// 超过CPU上限后,设置collide = false,为了让下次循环进入 else if (!collide)而不是进入下面的else if (cellsBusy == 0 && casCellsBusy()),防止进行扩容
                	// 跳到下面h = advanceProbe(h)位置,重新hash换一个槽位累加
                    collide = false;           
                else if (!collide)
                    collide = true;	
                else if (cellsBusy == 0 && casCellsBusy()) {// 其他线程没加锁,当前线程进入时再自己加锁
                    try {
                    	// 对cells进行扩容
                        if (cells == as) {      // Expand table unless stale
                            Cell[] rs = new Cell[n << 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
                }
                // 执行到这一步说明,前面的步骤都没成功,需要尝试换一个累加单元进行累加
                h = advanceProbe(h);
            }
            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
        }
    }

在这里插入图片描述

小结:longAccumulate方法流程图

在这里插入图片描述

(3)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;
    }
高并发下sum的值不精确的原因

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

  • 首先,最终返回的sum局部变量,初始被赋值为base,在最终返回时,可能有其他线程再次更新base,而此时局部变量sum不会更新,造成不一致
  • 其次,这里对cell的读取也无法保证是最后一次写入的值

(4)LongAdder 总结

  1. 采用分段CAS降低重试频率(这种分段的做法类似于JDK7中ConcurrentHashMap的分段锁)
  • 高并发环境下,value变量其实是一个热点数据,也就是N个线程竞争一个热点。
  • LongAdder的基本思路就是分散热点,将value值的新增操作分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个value值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。
  • LongAdder有一个全局变量volatile long base值,当并发不高的情况下都是通过CAS来直接操作base值,如果CAS失败,则针对LongAdder中的Cell[]数组中的Cell进行CAS操作,减少失败的概率。
  • 统计累加数据:sum() = base + Cell[]中的各Cell对象的value值之和。
  • 而AtomicLong是多个线程针对单个热点value值进行原子操作。
  1. 惰性求值
  • LongAdder只有在使用longValue()获取当前累加值时才会真正的去结算计数的数据,longValue()方法底层就是调用sum()方法,对base和Cell数组的数据累加然后返回,做到数据写入和读取分离。
  • 而AtomicLong使用incrementAndGet()每次都会返回long类型的计数值,每次递增后还会伴随着数据返回,增加了额外的开销。
  1. AtomicLong VS LongAdder

AtomicLong:

  • AtomicLong实现原理是基于CAS+自旋操作,CAS是基于硬件来实现原子性,保障线程安全。
  • AtomicLong使用场景:低并发下的全局计数器、序列号生成器。
  • AtomicLong优势:占用空间小;缺点:高并发下性能急剧下降(N个线程同时进行自旋,N-1个线程会自旋失败、不断重试)。

LongAdder:

  • LongAdder设计思想:空间换时间,分散value值的热点数据;实现原理:高并发时采用Cell数组进行分段CAS。
  • LongAdder使用场景:高并发下的全局计数器。
  • LongAdder优势:能减少CAS重试次数、能防止伪共享、惰性求值;缺点:使用sum统计时如果有并发更新,可能导致统计的数据有误差。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值