likes
comments
collection
share

Java并发编程之LongAdder源码(三)

作者站长头像
站长
· 阅读数 14

前言

前面两篇文章介绍了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可能会在baseCell数组两个地方进行累加操作,所以sum()方法就是对这两个的值进行求和。

long sum = base先获取base的值,然后判断Cell数组as是否为null,不为null就循环遍历数组每个元素,然后把值都加到sum上并返回。

@sun.misc.Contended

Java并发编程之LongAdder源码(三)

上图是CPU缓存设计概览,由于CPU和内存之间速度的差异太大,因此在CPU中设计了L1L2L3三级高速缓存,大小依次变大,速度依次变慢,其中L1L2是在每个内核中,而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数组时会大大提高计算效率。