Java并发编程之LongAdder源码(三)
前言
前面两篇文章介绍了LongAdder
的主要方法实现流程,除此之外还有两个需要注意的地方,分别是:
sum()
方法@sun.misc.Contended
注解
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()
方法的源码,通过前面两篇文章我们已经知道了LongAdder
可能会在base
和Cell
数组两个地方进行累加操作,所以sum()
方法就是对这两个的值进行求和。
long sum = base
先获取base
的值,然后判断Cell
数组as
是否为null
,不为null
就循环遍历数组每个元素,然后把值都加到sum
上并返回。
@sun.misc.Contended
上图是CPU
缓存设计概览,由于CPU
和内存之间速度的差异太大,因此在CPU
中设计了L1
、L2
、L3
三级高速缓存,大小依次变大,速度依次变慢,其中L1
、L2
是在每个内核中,而L3
是所有内核共享。
CPU
读取数据时,首先会从缓存中去读,如果缓存中没有的话会去内存先读到缓存中,然后再从缓存中取数据。而CPU
从内存中读取数据时并不是按字节或者单个字来读取的,因为根据局部性原理(一块内存被访问后,它附近的地址也很有可能会被访问),所以CPU
读取的是一整块内存(64
字节),然后保存到L3
中,这一块内存被称为缓存行(Cache Line
)。
而由于L3是所有内核共享的缓存,所以当一个内核修改了缓存行中的数据后,为了保证数据的一致性,会使其他内核中读取的该缓存行数据失效,就不得不从主存中重新读取数据,这种现象被称为伪共享。
为了解决伪共享的问题,可以采用“字节填充”的技术,其原理是通过在数据中填充空字节,尽可能让不相干的数据分布到不同的缓存行中,这样不同的CPU
读取的数据就在不同的缓存行了,避免了伪共享问题的发生。
LongAdder
之所以在高并发情况下效率会很高,主要是因为通过把累加操作分散到Cell
数组中每个元素上去,最后再进行求和来计算结果。
@sun.misc.Contended
static final class Cell {
// 省略...
}
在Cell
类上的注解@sun.misc.Contended
的作用就是通过字节填充避免缓存伪共享的产生,所以多个CPU
操作Cell
数组时会大大提高计算效率。
转载自:https://juejin.cn/post/7227038198408003641