注册

@Contended注解有什么用?

@Contended是Java 8中引入的一个注解,用于减少多线程环境下的“伪共享”现象,以提高程序的性能。

要理解@Contended的作用,首先要了解一下什么是伪共享(False Sharing)。

1. 什么是伪共享?

伪共享(False Sharing)是多线程环境中的一种现象,涉及到CPU的缓存机制和缓存行(Cache Line)。

现代CPU中,为了提高访问效率,通常会在CPU内部设计一种快速存储区域,称为缓存(Cache)。CPU在读写主内存中的数据时,会首先查看该数据是否已经在缓存中。如果在,就直接从缓存读取,避免了访问主内存的耗时;如果不在,则从主内存读取数据并放入缓存,以便下次访问。

缓存不是直接对单个字节进行操作的,而是以块(通常称为“缓存行”)为单位操作的。一个缓存行通常包含64字节的数据。

在多线程环境下,如果两个或更多的线程在同一时刻分别修改存储在同一缓存行的不同数据,那么CPU为了保证数据一致性,会使得其他线程必须等待一个线程修改完数据并写回主内存后,才能读取或者修改这个缓存行的数据。尽管这些线程可能实际上操作的是不同的变量,但由于它们位于同一缓存行,因此它们之间就会存在不必要的数据竞争,这就是伪共享。

伪共享会降低并发程序的性能,因为它会增加缓存的同步操作和主内存的访问。解决伪共享的一种方式是尽量让经常被并发访问的变量分布在不同的缓存行中,例如,可以通过增加无关的填充数据,或者利用诸如Java的@Contended注解等工具。

2. @Contended注解是什么?

@Contended 是Java 8引入的一个注解,设计用于减少多线程环境下的伪共享(False Sharing)问题以提高程序性能。

伪共享是现代多核处理器中一个重要的性能瓶颈,它发生在多个处理器修改同一缓存行(Cache Line)中的不同数据时。缓存行是内存的基本单位,一般为64字节。当一个处理器读取主内存中的数据时,它会将整个缓存行(包含需要的数据)加载到本地缓存(L1,L2或L3缓存)中。如果另一个处理器修改了同一缓存行中的其他数据,那么原先加载到缓存中的数据就会变得无效,需要重新从主内存中加载。这会增加内存访问的延迟,降低程序性能。

@Contended注解可以标注在字段或者类上。它能使得被标注的字段在内存布局上尽可能地远离其他字段,使得被标注的字段或者类中的字段分布在不同的缓存行上,从而减少伪共享的发生。

例如,考虑以下代码:

public class Foo {
@Contended
long x;
long y;
}

在这里,x@Contended注解标记,所以xy可能会被分布在不同的缓存行上,这样如果多个线程并发访问xy,就不会引发伪共享。

需要注意的是,@Contended是JDK的内部API,它在Java 8中引入,但在默认情况下是不开放的,要使用需要添加JVM参数-XX:-RestrictContended,并且在编译时需要使用--add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED。此外,过度使用@Contended可能会浪费内存,因为它会导致大量的内存空间被用作填充以保持字段间的距离。所以在使用时需要谨慎权衡内存和性能的考虑。

3. 简单案例

在Java 8及以上版本中,@Contended注解是属于jdk的内部API,因此在正常情况下使用时需要打开开关-XX:-RestrictContended才能正常使用。同时需要注意的是,@Contended在JDK 9以后的版本中可能无法正常工作,因为JDK 9开始禁止使用Sun的内部API。

以下是一个@Contended注解的简单使用案例:

import jdk.internal.vm.annotation.Contended;

public class ContendedExample {

@Contended
volatile long value1 = 0L;

@Contended
volatile long value2 = 0L;

public void increaseValue1() {
value1++;
}

public void increaseValue2() {
value2++;
}

public static void main(String[] args) {
ContendedExample example = new ContendedExample();

Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
example.increaseValue1();
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
example.increaseValue2();
}
});

thread1.start();
thread2.start();

try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("value1: " + example.value1);
System.out.println("value2: " + example.value2);
}
}

这个例子中定义了两个使用了@Contended注解的volatile长整型字段value1value2。两个线程分别对这两个字段进行增加操作。因为这两个字段使用了@Contended注解,所以他们会被分布在不同的缓存行中,减少了因伪共享带来的性能问题。但由于伪共享的影响在实际运行中并不容易直接观察,所以这个例子主要展示了@Contended注解的使用方式,而不是实际效果。


作者:一只爱撸猫的程序猿
链接:https://juejin.cn/post/7255604228956110904
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册