LongAccumulator是如何实现的,因此效率更高?

时间:2014-04-10 06:49:11

标签: java concurrency atomic java-8

据我所知,新的Java(8)引入了新的同步工具,例如LongAccumulator(在原子包下)。

在文档中,它说当多个线程的变量更新频繁时,LongAccumulator效率更高。

我想知道它是如何实现更高效的?

2 个答案:

答案 0 :(得分:22)

这是一个非常好的问题,因为它显示了使用共享内存进行并发编程的一个非常重要的特性。在进入细节之前,我必须退后一步。看看下面的课程:

class Accumulator {
    private final AtomicLong value = new AtomicLong(0);
    public void accumulate(long value) {
        this.value.addAndGet(value);
    }
    public long get() {
        return this.value.get();
    }
}

如果您创建此类的一个实例并在循环中从一个线程调用方法accumulate(1),那么执行将非常快。但是,如果从两个线程在同一个实例上调用该方法,则执行将大约两个幅度

你必须看一下内存架构才能理解会发生什么。现在大多数系统都有non-uniform memory access。特别地,每个核心具有其自己的L1高速缓存,其通常被构造为具有64个八位字节的高速缓存行。如果内核在内存位置上执行原子增量操作,则首先必须获得对相应缓存行的独占访问权限。由于需要与所有其他核心协调,如果它还没有独占访问权限那么昂贵。

解决这个问题有一个简单而反直觉的技巧。看看下面的课程:

class Accumulator {
    private final AtomicLong[] values = {
        new AtomicLong(0),
        new AtomicLong(0),
        new AtomicLong(0),
        new AtomicLong(0),
    };
    public void accumulate(long value) {
        int index = getMagicValue();
        this.values[index % values.length].addAndGet(value);
    }
    public long get() {
        long result = 0;
        for (AtomicLong value : values) {
            result += value.get();
        }
        return result;
    }
}

乍一看,由于额外的操作,这个类似乎更贵。但是,它可能比第一类快几倍,因为它具有更高的概率,即执行核心已经独占访问所需的缓存行。

为了实现这一目标,你必须考虑更多的事情:

  • 不同的原子计数器应位于不同的缓存行上。否则,您将一个问题替换为另一个问题,即false sharing。在Java中,您可以使用long[8 * 4]来实现此目的,并且仅使用索引081624
  • 必须明智地选择计数器的数量。如果不同的计数器太少,则仍有太多的缓存切换。如果计数器太多,则会浪费L1缓存中的空间。
  • 方法 getMagicValue 应该返回一个与核心ID具有亲缘关系的值。

总之, LongAccumulator 对某些用例更有效,因为它使用冗余内存进行常用的写操作,为了减少次数,必须更换缓存行核心之间。另一方面,读取操作稍微昂贵一些,因为它们必须创建一致的结果。

答案 1 :(得分:1)