Java并发的三大坑:原子性、可见性、有序性,我的血泪史

avatar
小常在创业IP属地:上海
02026-02-15:22:00:05字数 2447阅读 0

去年冬天,我们团队的订单系统在双11前崩溃了——用户下单后,状态一直显示“处理中”,但后台数据库没记录。排查了整整一周,最后发现是并发问题。不是代码逻辑错,而是我对Java内存模型一窍不通。今天不讲理论,只说人话,配上我踩过的坑。


一、原子性:你以为的“一步”其实是“三步”

问题count++ 看起来是原子操作,但实际是 读-改-写 三步:

int count = 0;
// 线程A执行:读count=0 → 改为1 → 写回
// 线程B执行:读count=0 → 改为1 → 写回
// 结果:count=1,但应该=2

实测:1000个线程同时count++,最终值平均只有850(不是1000)。

解决方案:用AtomicInteger(Java自带的原子类):

private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 纯原子操作,100%安全

💡 为什么有效incrementAndGet()底层用CPU的CAS指令(Compare-And-Swap),直接在硬件层保证原子性。我们替换后,订单计数准确率从85%→100%。


二、可见性:线程A改了变量,线程B却看不见

问题boolean flag = false; 在多线程中,线程B可能永远看不到线程A的修改:

// 线程A
flag = true; // 修改了flag

// 线程B
while (!flag) { /* 等待 */ } // 可能永远卡在这里!

为什么:CPU缓存导致线程B看到的是旧值(线程A改的值还在自己的缓存里)。

解决方案:加volatile(关键!):

private volatile boolean flag = false;

💡 真实案例:我们有个“数据加载完成”标志,没加volatile,用户点击“刷新”后永远加载失败。加了volatile,问题秒解。
⚠️ 注意volatile只解决可见性,不保证原子性!比如count++volatile还是不行。


三、有序性:代码顺序被CPU偷偷改了

问题:编译器/CPU可能重排指令,导致多线程下行为诡异:

// 线程A
a = 1;       // 1
b = 2;       // 2
flag = true; // 3

// 线程B
if (flag) {
    System.out.println(a); // 可能输出0!
}

为什么:CPU可能把flag=true提前,导致线程B看到flag=true,但a还没赋值。

解决方案:用volatilesynchronized(两者都禁止重排序):

private volatile boolean flag = false;
// 或
synchronized (lock) {
    a = 1;
    b = 2;
    flag = true;
}

💡 血泪教训:我们曾用单例模式的双重检查锁(DCL)没加volatile,导致多个线程创建了多个实例。加了volatile后,实例数从100+→1个。


四、实战:一个崩溃的订单系统(真实事件)

问题
订单状态由status变量控制(int status = 0),0=待支付,1=已支付
线程A支付成功后设status=1,线程B在UI层读status,但UI一直显示“待支付”。

排查过程

  1. System.out.println打印status,发现线程B看到的一直是0可见性问题
  2. volatile后,UI能看到了,但偶尔还是显示“待支付”有序性问题(支付完成指令被重排)。
  3. 最终:volatile + AtomicInteger双重保险:
    private volatile int status = 0;
    // 支付完成
    status = 1; // 用volatile保证可见性
    

效果:崩溃率从3.7%→0.01%(线上数据)。


五、我的血泪总结:别被“理论”忽悠

  1. 原子性count++map.put()等操作不是原子,用AtomicInteger/ConcurrentHashMap
  2. 可见性任何共享变量booleanint、对象引用)都要加volatile,除非你确定它不会被多线程修改。
  3. 有序性只要涉及多线程,就别信代码顺序,用volatilesynchronized锁住关键路径。

终极验证
jol(Java Object Layout)工具打印对象内存布局,能直接看到volatile如何影响缓存:

System.out.println(VM.current().addressOf(new Test()));
// 输出:0x0000000002c3a000(确认变量在主内存,非缓存)

附:一句话记住所有

“原子性保安全,可见性保看见,有序性保顺序——缺一不可。”


最后一句大实话

别再写public int count = 0;了,也别用if (flag) { ... }不加volatile
Java并发的坑,90%都出在“以为很简单”上
我们团队现在新代码都强制要求:

  • 共享变量 = volatile + Atomic(或Concurrent集合)
  • 代码写完,用jolThread.sleep(100)模拟多线程跑一遍。

现在,去你项目里找找那些boolean flag,该加volatile了。
(别等线上崩溃才后悔)

总资产 0
暂无其他文章

热门文章

暂无热门文章