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还没赋值。
解决方案:用volatile或synchronized(两者都禁止重排序):
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一直显示“待支付”。
排查过程:
- 用
System.out.println打印status,发现线程B看到的一直是0→ 可见性问题。 - 加
volatile后,UI能看到了,但偶尔还是显示“待支付” → 有序性问题(支付完成指令被重排)。 - 最终:
volatile+AtomicInteger双重保险:private volatile int status = 0; // 支付完成 status = 1; // 用volatile保证可见性
效果:崩溃率从3.7%→0.01%(线上数据)。
五、我的血泪总结:别被“理论”忽悠
- 原子性:
count++、map.put()等操作不是原子,用AtomicInteger/ConcurrentHashMap。 - 可见性:任何共享变量(
boolean、int、对象引用)都要加volatile,除非你确定它不会被多线程修改。 - 有序性:只要涉及多线程,就别信代码顺序,用
volatile或synchronized锁住关键路径。
✅ 终极验证:
用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集合) - 代码写完,用
jol或Thread.sleep(100)模拟多线程跑一遍。
现在,去你项目里找找那些boolean flag,该加volatile了。
(别等线上崩溃才后悔)
