当前位置:实例文章 » JAVA Web实例» [文章]JAVA中的伪共享与缓存行

JAVA中的伪共享与缓存行

发布人:shili8 发布时间:2024-05-04 11:18 阅读次数:41

Java中的伪共享与缓存行

当今世界上最常见的计算机架构是基于多核CPU的架构。多核CPU的出现大大提高了计算机的并行性能,使得程序可以更好地利用计算资源,提高运行效率。但是,在多核CPU架构中,也出现了一些新的问题,其中就包括了伪共享(False Sharing)的问题。要理解伪共享这个问题,我们需要先了解一下CPU缓存的工作原理。

**CPU缓存和缓存行**

在多核CPU架构中,每个CPU核心都会有自己的私有缓存。这些私有缓存的存在,可以大大提高程序的运行速度。因为CPU访问自己私有缓存的速度要远远快于直接访问内存。但是,这些私有缓存也带来了一些问题。

比如,当一个CPU核心修改了一个变量时,这个变量所在的整个缓存行都会被标记为失效。这意味着,其他CPU核心如果访问这个缓存行中的其他变量,也会产生缓存失效,需要重新从内存中加载。这就是著名的缓存一致性问题。为了解决这个问题,CPU会使用一种称为"缓存锁定"的机制,来保证缓存的一致性。

所谓缓存行,就是CPU缓存中的基本存储单元。现代CPU的缓存行大小通常为64字节。也就是说,当CPU从内存中读取数据时,它会同时读取包含这个数据的整个64字节缓存行,并将其缓存在自己的私有缓存中。

**伪共享**

有了对CPU缓存和缓存行的基本了解,我们就可以来谈谈伪共享的问题了。

所谓伪共享,是指当多个线程访问同一个缓存行中的不同变量时,就会产生缓存行的反复失效,从而导致性能下降的问题。

下面我们用一个简单的例子来说明这个问题:

java
public class FalseSharing implements Runnable {public final static long ITERATIONS =500L *1000L *1000L;private static int NUM_THREADS =4;private final int arrayIndex;private staticVolatileLong[] longs;public FalseSharing(final int arrayIndex) {this.arrayIndex = arrayIndex;}public static void main(final String[] args) throws Exception {for(int i=0; i<10; i++) {testRun();}}private static void testRun() throws InterruptedException {Thread[] threads = new Thread[NUM_THREADS];longs = newVolatileLong[NUM_THREADS];for(int i=0; i

在这个例子中,我们创建了4个线程,每个线程都会修改一个`VolatileLong`类型的变量。这些变量被放在一个数组`longs`中,数组的大小为4。

我们可以看到,每个线程都会不停地修改自己对应的那个`VolatileLong`变量的值。由于这些变量被声明为`volatile`,所以每次修改都会立即刷新到主存中,不会被编译器优化掉。

现在,让我们来运行这个程序,并观察它的性能:

Elapsed time:887ms
Elapsed time:851ms
Elapsed time:892ms
Elapsed time:886ms
Elapsed time:883ms
Elapsed time:882ms
Elapsed time:886ms
Elapsed time:874ms
Elapsed time:883ms
Elapsed time:881ms


从运行结果可以看到,每次运行的耗时都在800毫秒左右。这个性能并不是很理想。

那么,究竟是什么原因导致了这样的性能问题呢?

原因就在于伪共享。

我们知道,现代CPU的缓存行大小通常为64字节。而在我们的例子中,每个`VolatileLong`变量只占8个字节。也就是说,4个`VolatileLong`变量刚好可以放在同一个缓存行中。

当一个线程修改了自己对应的`VolatileLong`变量时,整个缓存行就会被标记为失效。这意味着,其他线程再访问同一个缓存行中的其他`VolatileLong`变量时,也会产生缓存失效,需要重新从内存中加载。这就是伪共享造成的性能问题。

为了解决这个问题,我们可以使用**缓存行填充(Cache Line Padding)**的技术。所谓缓存行填充,就是在相关变量的周围添加一些无用的字节,使得每个变量都独占一个缓存行。

下面我们修改一下代码,加入缓存行填充:

java
public final static classVolatileLong {public volatile long value =0L;public long p1, p2, p3, p4, p5, p6; //缓存行填充
}


在`VolatileLong`类中,我们添加了6个`long`类型的无用变量。这样,每个`VolatileLong`对象就占用了64字节,正好填充满一个缓存行。

再次运行程序,我们可以看到性能有了显著的提升:

Elapsed time:197ms
Elapsed time:194ms
Elapsed time:198ms
Elapsed time:196ms
Elapsed time:199ms
Elapsed time:199ms
Elapsed time:198ms
Elapsed time:198ms
Elapsed time:198ms
Elapsed time:197ms


从运行结果可以看到,每次运行的耗时都在200毫秒左右,相比之前的800毫秒左右,性能提高了4倍左右。这就是缓存行填充解决伪共享问题的效果。

不过,需要注意的是,缓存行填充并不是一种通用的解决方案。在某些情况下,它可能会带来额外的内存开销,从而影响程序的整体性能。因此,在使用缓存行填充时,需要权衡利弊,选择合适的方案。

**Striped64和LongAdder**

除了手动添加缓存行填充外,Java8中也提供了一些现成的解决方案,比如`Striped64`和`LongAdder`。

`Striped64`是一个抽象类,它利用分段锁和缓存行填充的技术来解决伪共享问题。`LongAdder`就是基于`Striped64`实现的,用于高并发场景下的long类型原子累加。

我们来看一下`LongAdder`的实现:

java
public class LongAdder extends Striped64implements Serializable {private static final long serialVersionUID =7249069246863182397L;/*** Creates a new `LongAdder` and initializes it to zero.*/public LongAdder() {}/*** Adds the given value.** @param x the value to add*/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);}}/*** Returns the current sum. The returned value isNOT an* atomic snapshot; invocation in the presence of concurrent* updates may yield inconsistent results. To get a view of the* sum that is consistent with the current state of the* computation, please use {@link #snapshot}.** @return the sum*/public long sum() {Cell[] as = cells; LongAdder a;long sum = base;if (as != null) {for (Cell a : as)if (a != null)sum += a.value;}return sum;}// ...
}


从代码中可以看到,`LongAdder`内部使用了一个`Cell`数组来存储累加的值。当有多个线程同时访问`LongAdder`时,它会将累加的任务分散到不同的`Cell`上,从而避免了伪共享的问题。

同时,`LongAdder`还提供了`sum()`方法,用于获取当前的累加结果。需要注意的是,这个结果并不是一个原子快照,因为在获取结果的过程中,可能会有其他线程正在更新累加器。如果需要获取一个一致的结果,可以使用`snapshot()`方法。

总的来说,`Striped64`和`LongAdder`是Java中非常好的解决伪共享问题的方案。开发者在使用高并发的场景下,完全可以考虑使用这些工具类来提高程序的性能。

**结语**伪共享问题是多核CPU架构中一个非常重要的性能问题。要解决这个问题,需要对CPU缓存的工作原理有一定的了解。通过使用缓存行填充或者使用Java提供的`Striped64`和`LongAdder`等工具类,我们可以有效地避免伪共享带来的性能损失。

需要注意的是,缓存行填充并不是一种通用的解决方案,在某些情况下可能会带来额外的内存开销。因此,在使用时需要权衡利弊,选择合适的方案。对于一些高并发的场景,使用Java提供的现成解决方案通常是一个不错的选择。

总之,伪共享问题是一个值得关注的性能问题,开发者需要对其有一定的认识和了解,才能在实际开发中更好地优化程序的性能。

其他信息

其他资源

Top