网站优化

网站优化

Products

当前位置:首页 > 网站优化 >

Java原子操作中,如何避免伪共享导致的并发问题?

GG网络技术分享 2026-04-15 19:09 2


啥玩意儿? 作为一个在代码世界摸爬滚打多年的老兵,我最近又经历了一场“并发噩梦”。事情的起因是项目中需要统计用户访问量,自然就想到了使用Java的原子类来保证线程平安。只是实际测试的时候却发现计数后来啊经常不准确,这简直让人抓狂!我开始怀疑人生了……难道是我的代码有问题?还是Java的原子类本身就有缺陷?

起初的疑惑:AtomicInteger为何失灵?

最开始的代码非常简单, 就是一个Counter类,内部使用AtomicInteger来存储计数器:

public class Counter { private AtomicInteger count = new AtomicInteger; public void increment { count.incrementAndGet; } public int getCount { return count.get; }}

这段代码看起来没什么问题啊!AtomicInteger不是线程平安的吗?为什么在高并发场景下会丢失计数呢?我反复检查了几遍代码,确认没有其他地方修改了count的值。难道是幻觉吗?不对劲啊!

尝试升级JVM版本

为了排除JVM版本的影响,我尝试升级到最新的Java 17。后来啊呢?依旧无效!问题依然存在。难道真是我的代码有问题吗?不甘心啊!我决定深入研究一下AtomicInteger的底层实现。

CAS失败与潜在竞态条件

在我看来... 这种实现方式与AtomicInteger的底层逻辑相似, 但后来啊依然相同,说明问题并非出多个线程一边尝试更新同一个值,导致部分更新被覆盖。

这时候, 我开始怀疑是否是JVM的内存模型或编译优化导致的问题,或者是否存在其他隐藏的竞态条件,闹乌龙。。

但是在我的测试中,还是出现了部分计数丢失。这让我开始怀疑是否有其他因素干扰了CAS操作。

简单的验证实验

public void increment { int current = count.get; int next = current + 1; boolean success = count.compareAndSet; if  { System.err.println; }}

发现部分请求的计数出现了不一致的情况,比如实际访问次数比预期少。 C位出道。 这明显不是预期的行为,于是开始深入排查。

伪共享:隐藏的性能杀手

产品名称功能适用场景价格
Intel Core i9高性能CPU服务器、 游戏$500-$800
AMD Ryzen 9多核CPU内容创作、科学计算$400-$700
Samsung SSD 980 Pro高速固态硬盘操作系统、游戏、数据库$150-$300
经过一番研究和分析后我终于发现了问题的根源——伪共享! 原来是这样:虽然AtomicInteger保证了对单个整数变量的操作是原子性的, 但是如果多个线程一边修改位于同一个缓存行上的不同变量,就会发生伪共享现象.

比如,即使每个线程操作的是不同的 `AtomicInteger`实例, 如果这些实例恰好位于同一个缓存行上,那么它们之间的修改会导致缓存行的无效化和重新加载,从而降低性能并可能导致数据不一致.,我是深有体会。

什么是缓存行?

缓存行是 CPU 和主内存之间数据传输的基本单位。当一个线程修改了缓存行中的某个数据时,整个缓存行都会被标记为脏数据并写入主内存。 好吧... 如果另一个线程需要访问同一个缓存行中的其他数据,它必须先从主内存加载该缓存行.

解决方案:填充与对齐

为了避免伪共享,可以采用以下两种方法:

  • 填充 在每个 `AtomicInteger`实例前面添加一些填充字段,以确保它们位于不同的缓存行上
  • 对齐 使用 `@sun.misc.Contended` 注解来显式地将 `AtomicInteger`实例分配到不同的缓存行上

使用填充字段

java public class CounterWithPadding { private long p1, p2, p3, p4, p5 , p6 ; private volatile int count = new AtomicInteger; private long p7 , p8 , p9 , p10 , p11 ; public void increment{ count.incrementAndGet;} public int getCount{ return count.get;} }

@Contended注解

java import sun.misc.Contended import java .util .concurrent .atomic .Atomic Integer ; @Contended public class CounterWithAnnotation{ private volatile @Contended Atomic Integer count = new Atomic Integer ; public void increment{ count .incrementAndGet;} public int getCount{ return count .get;} }

案例分析与经验

技术方案优点缺点适用场景
synchronized简单易用、 兼容性好性能较低、容易死锁低并发场景、简单同步需求
Lock接口 灵活可控、可重入性好复杂性较高、容易出错高并发场景、复杂的同步需求
Atomic类 高性能、无锁编程理念、减少锁竞争风险只能用于简单的数据操作、可能存在ABA问题和伪共享问题 。需要注意内存可见性和有序性 。如果你的程序运行环境经常出现频繁上下文切换或者 CPU 的 NUMA架构 ,那么可能会遇到一些性能瓶颈 。主要原因是在这种情况下 ,每个核心都对应着独立的内存区域 ,而跨核心访问内存可能会变得比较慢 。所以呢 ,你应该尽量将你的数据放在靠近 CPU 的本地内存中 。如果你的应用程序依赖于大量的数据结构或者复杂的算法 ,那么使用无锁编程可能会带来更高的复杂度 。你需要仔细设计你的算法和数据结构 ,以确保它们能够正确地工作 。如果你需要处理非常复杂的同步需求 ,那么最好还是使用传统的锁机制 。主要原因是锁机制可以提供更强的控制力和灵活性 。在使用无锁编程的时候 ,你需要特别注意 ABA 问题 、内存可见性和有序性等问题 。ABA 问题是指一个变量的值从 A 修改为 B 再修改回 A 的情况 ,而 CAS 操作无法检测到这种变化 。内存可见性是指一个线程对变量的修改是否能够被其他线程看到 。有序性是指指令施行的顺序是否符合程序的预期顺序 。所以呢 ,在使用无锁编程的时候 ,你需要仔细考虑这些因素 ، 并采取相应的措施来避免这些问题。记住在使用 atomic 类时也要注意潜在的问题!比方说 ABA 问题 !

我无法认同... 总之这次经历让我更加重视并发编程中的细节问题也让我意识到即使是看似简单的代码也可能隐藏着复杂的隐患.

希望这篇文章能帮助你在 Java 并发编程的世界里少走弯路!


提交需求或反馈

Demand feedback