Java8中@Contended和伪共享

Java8引入了@Contended注解以减少伪共享的影响。伪共享是指多个线程因访问同一缓存行而产生的性能损耗。文章介绍了缓存行的概念、伪共享的原因及其解决方案,包括使用填充技术及@Contended注解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

@Contended和伪共享


原文:http://jingege.me/2016/05/31/sharing-false-and-contented/

原文: http://robsjava.blogspot.com/2014/03/what-is-false-sharing.html

Java8引入了@Contented这个新的注解来减少伪共享(False Sharing)的发生。本文介绍了@Contented注解并解释了为什么False Sharing是如何影响性能的。

缓存行

CPU读取内存数据时并非一次只读一个字节,而是会读一段64字节长度的连续的内存块(chunks of memory),这些块我们称之为缓存行(Cache line)。

假设你有两个线程(Thread1和Thread2)都会修改同一个volatile变量x:

      
1
      
volatile long x;

如果Thread1先改变x的值,然后Thread2又去读它:

      
1
2
      
Thread 1: x= 3;
Thread 2: System.out.print(x);

那么x所在缓存行上的所有64个字节的值都要被重新加载,因为CPU核心间交换数据是以缓存行为最小单位的。当然Thread1和Thread2是有可能在同一个核心上运行的,但我们此处假设两个线程在不同的核心上运行。

已知long类型占8个字节,缓存行长度为64个字节,那么一个缓存行可以保存8个long型变量,我们已经有了一个long型的x,假设x所在缓存行里还有其他7个long型变量,v1到v7:

      
1
      
x, v1, v2, v3, v4, v5 , v6 , v7

伪共享(False Sharing)

这个缓存行可以被许多线程访问。如果其中一个修改了v2,那么会导致Thread1和Thread2都会重新加载整个缓存行。你可能会疑惑为什么修改了v2会导致Thread1和Thread2重新加载该缓存行,毕竟只是修改了v2的值啊。虽然说这些修改逻辑上是互相独立的,但同一缓存行上的数据是统一维护的,一致性的粒度并非体现在单个元素上。这种不必要的数据共享就称之为“伪共享”(False Sharing)。

填充(Padding)

一个CPU核心在加载一个缓存行时要执行上百条指令。如果一个核心要等待另外一个核心来重新加载缓存行,那么他就必须等在那里,称之为stall(停止运转)。减少伪共享也就意味着减少了stall的发生,其中一个手段就是通过填充(Padding)数据的形式,来保证本应有可能位于同一个缓存行的两个变量,在被多线程访问时必定位于不同的缓存行。

在下面这个例子里,我们试图通过填充的方式,使得xv1位于不同的缓存行:

      
1
2
3
4
5
6
7
8
9
10
11
      
public class FalseSharingWithPadding {
public volatile long x;
public volatile long p2; // padding
public volatile long p3; // padding
public volatile long p4; // padding
public volatile long p5; // padding
public volatile long p6; // padding
public volatile long p7; // padding
public volatile long p8; // padding
public volatile long v1;
}

在你考虑使用填充之前,必须要了解的一点是JVM可能会清除无用字段或重排无用字段的位置,这样的话,可能无形中又会引入伪共享。我们也没有办法指定对象在堆内驻留的位置。

为了避免无用字段被消除,通常我们会用volatile修饰一下。个人建议只需为处于激烈竞争状态的类进行填充处理,而且一般只有通过对其性能分析才能发现性能上的不同。通常在性能分析时,最好在对其迭代访问10000次之后再去采样,这样可以消除JVM本身的运行时优化策略带来的影响。

Java8和@Contended

除了对字段进行填充之外,还有一个比较清爽的方法,那就是对需要避免陷入伪共享的字段进行注解,这个注解暗示JVM应当将字段放入不同的缓存行,这也正是JEP142的相关内容。

该JEP引入了@Contented注解。被这个注解修饰的字段应当和其他的字段驻留在不同的位置。

      
1
2
3
4
5
      
public class Point {
int x;
@Contended
int y;
}

上面的代码将x和y置于不同的缓存行。@Contented注解将y移动到远离对象头部的地方,(以避免和x一起被加载到同一个缓存行)。

参考

http://openjdk.java.net/projects/jdk8/features
http://beautynbits.blogspot.co.uk/2012/11/the-end-for-false-sharing-in-java.html
http://openjdk.java.net/jeps/142
http://mechanical-sympathy.blogspot.co.uk/2011/08/false-sharing-java-7.html
http://stackoverflow.com/questions/19892322/when-will-jvm-use-intrinsics
https://blogs.oracle.com/dave/entry/java_contented_annotation_to_help

<think>我们正在讨论的是Java中的`@Contended`注解(注意:正确的注解名是`jdk.internal.vm.annotation.Contended`,通常简称为`Contended`)。这个注解Java虚拟机(JVM)内部使用的一个注解,用于解决共享(False Sharing)问题。下面我将详细解释其作用使用场景。 ### 作用 `@Contended`注解的主要作用是**避免共享**(False Sharing)。共享是多线程编程中的一种性能问题,它发生在多个线程同时修改位于同一缓存行(Cache Line)中的不同变量时。由于现代CPU的缓存系统是以缓存行为单位进行操作的(通常缓存行大小为64字节),当多个线程修改同一缓存行中的不同变量时,会导致缓存行的无效化,从而强制其他线程重新从主内存中加载数据,造成性能下降。 `@Contended`注解通过在被注解的字段前后添加填充(Padding)字节,确保该字段独占一个缓存行,从而避免与其他字段发生共享。 ### 使用场景 1. **高性能并发数据结构**:例如在`java.util.concurrent`包中的一些类(如`ConcurrentHashMap`、`LongAdder`等)中,用于标记那些可能被多个线程频繁修改的字段。 2. **自定义高并发类**:当你在开发多线程应用时,如果发现某些字段被多个线程频繁修改,并且这些字段位于同一个缓存行中,可以使用`@Contended`来优化性能。 ### 使用方法 由于`@Contended`是JDK内部API,位于`jdk.internal.vm.annotation`包中,因此通常不推荐在应用程序代码中直接使用。如果确实需要使用,可以通过以下方式: 1. **添加JVM参数**:在启动JVM时添加`-XX:-RestrictContended`参数,以允许在用户代码中使用`@Contended`注解。 2. **导入注解**:在代码中导入`jdk.internal.vm.annotation.Contended`。 示例代码: ```java import jdk.internal.vm.annotation.Contended; public class Counter { // 使用@Contended注解避免共享 @Contended private volatile long count1 = 0; @Contended private volatile long count2 = 0; // ... 其他代码 } ``` ### 注意事项 1. **JDK版本要求**:`@Contended`注解Java 8开始引入。 2. **填充大小**:默认情况下,JVM会在被注解的字段前后添加128字节的填充(即整个字段占用128字节,足以覆盖大多数CPU的缓存行大小)。也可以通过`-XX:ContendedPaddingWidth`参数调整填充大小。 3. **谨慎使用**:过度使用`@Contended`会导致内存占用增加,因为每个被注解的字段都会独占一个缓存行(至少64字节)。因此,只应在性能关键且确实存在共享问题的场景中使用。 ### 示例说明 考虑以下场景:两个线程分别频繁修改两个不同的`long`变量(`count1``count2`)。如果这两个变量位于同一缓存行中,就会发生共享。使用`@Contended`注解后,每个变量都会独占一个缓存行,从而消除共享。 ### 性能影响 使用`@Contended`可以显著提升多线程程序的性能,特别是在高并发场景下。例如,在`java.util.concurrent`包中的`LongAdder`类中,其内部使用的`Cell`数组就使用了`@Contended`注解[^4]。 --- §§相关问题§§ 1. 什么是共享(False Sharing)?如何检测共享问题? 2. 除了使用`@Contended`注解,还有哪些方法可以避免共享? 3. Java内存布局中对象头的作用是什么? 4. 如何通过JVM参数调整`@Contended`的填充大小? 5. `volatile`关键字与缓存一致性协议(如MESI)的关系是什么? [^1]: 所有文字字符串字符串值常量表达式都会被驻留(interned)。字符串字面量在Java™语言规范的第3.10.5节中定义。`public native String intern();` [^2]: 展示了使用Unsafe进行CAS操作的示例代码,通过反射获取Unsafe实例,然后对对象字段进行原子更新。 [^3]: 演示了`LockSupport.park()``LockSupport.unpark()`的使用,即使先调用`unpark()`再调用`park()`,线程也不会被阻塞。 [^4]: `LongAdder`的`add()`方法中,当存在竞争时,会使用`Cell`数组分散竞争,而`Cell`类使用了`@Contended`注解避免共享。</think>### Java `@Contended` 注解的作用与使用场景 #### 作用 `@Contended` 是 Java 虚拟机(JVM)内部的注解(位于 `jdk.internal.vm.annotation` 包),主要解决**共享(False Sharing)** 问题,通过**内存填充**确保被注解字段独占 CPU 缓存行(Cache Line),从而提升多线程并发性能[^1][^2]。 #### 核心原理 1. **共享问题** 当多个线程频繁修改位于同一缓存行(通常 64 字节)的不同变量时,CPU 缓存频繁失效,导致性能下降。 2. **解决方案** `@Contended` 在字段前后添加**填充字节**(Padding),强制该字段独占整个缓存行: ```java class Data { @Contended // 添加填充字节 volatile long value1; @Contended volatile long value2; } ``` - 未使用注解:`value1` `value2` 可能共享同一缓存行 - 使用注解后:每个字段独占一个缓存行 #### 使用场景 1. **高并发计数器** 如 `java.util.concurrent.atomic.LongAdder` 中的 `Cell` 类[^4]: ```java @jdk.internal.vm.annotation.Contended static final class Cell { volatile long value; //... } ``` 2. **性能关键数据结构** - 线程局部变量(Thread-Local Storage) - 高频修改的共享状态字段 - 锁状态标志(如 AQS 中的同步状态) 3. **避免缓存行竞争** 当多个线程密集修改相邻内存位置的变量时(如数组元素) #### 使用方法 1. **启用注解** 需添加 JVM 参数: ```bash -XX:-RestrictContended # 允许用户代码使用 ``` 2. **代码示例** ```java import jdk.internal.vm.annotation.Contended; public class Counter { @Contended("group1") // 可选分组 private volatile long count1; @Contended("group2") private volatile long count2; } ``` #### 注意事项 1. **内存开销** 每个 `@Contended` 字段增加约 128 字节填充(可通过 `-XX:ContendedPaddingWidth` 调整) 2. **适用场景** 仅推荐在**极端性能优化**时使用,普通业务代码无需关注 3. **JDK 版本** 从 Java 8 开始支持,但属于**内部 API**,未来可能变更 4. **替代方案** 手动填充(兼容性更好): ```java class ManualPadding { volatile long value; long p1, p2, p3, p4, p5, p6, p7; // 填充 56 字节 } ``` #### 性能对比 | **场景** | 未使用 `@Contended` | 使用 `@Contended` | |----------------|---------------------|------------------| | 缓存未命中率 | 高(共享) | 低 | | 吞吐量 | 下降 30%-50% | 接近线性扩展 | | 内存占用 | 正常 | 增加 128 字节/字段 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值