volatile 是 Java 并发里经常被误用的关键字。它能保证变量修改对其他线程可见,并禁止相关指令重排,但它不是互斥锁,也不能保证复合操作的原子性。
供应链系统中,volatile 最适合的场景是任务开关、配置引用、状态标记。比如库存同步任务需要能被后台管理页面停止,或者定时任务需要感知配置已刷新。
JMM 要解决什么
Java 内存模型规定了线程如何从主内存读取变量、如何把修改写回主内存,以及什么情况下一个线程的写入对另一个线程可见。
如果没有同步手段,一个线程修改变量,另一个线程不一定马上看到。CPU 缓存、编译器优化、指令重排都会让多线程程序表现得不符合直觉。
库存同步任务的停止标志是典型例子:
1 | public class InventorySyncTask implements Runnable { |
管理线程调用 stop() 后,工作线程可能仍然看不到 running = false,导致任务停不下来。
使用 volatile 修复可见性
1 | public class InventorySyncTask implements Runnable { |
volatile 写入会把修改刷新出去,volatile 读取会从可见位置重新读取。这样管理线程修改停止标志后,工作线程能及时观察到。
这类代码在供应链系统中很常见:库存同步、价格同步、承运商轨迹拉取、供应商主数据刷新,都可能需要停止标志。
volatile 不保证 count++ 安全
错误示例:
1 | public class SyncMetrics { |
successCount++ 不是一个原子操作,它包含读取、加一、写回。多个线程同时执行时会丢失更新。
正确写法可以用 AtomicInteger:
1 | public class SyncMetrics { |
如果是高并发统计,比如每秒记录库存同步成功数,用 LongAdder 更合适:
1 | public class SyncMetrics { |
配置引用的安全发布
volatile 还适合发布不可变配置对象。比如供应链系统里有库存分配策略:
1 | public class AllocationPolicyHolder { |
只要 AllocationPolicy 本身是不可变对象,刷新时整体替换引用,读线程就能看到完整的新配置。
不要这样做:
1 | policy.getRules().add(newRule); |
如果对象内部可变,即使引用是 volatile,内部集合的并发修改仍然不安全。配置对象应设计成不可变:
1 | public final class AllocationPolicy { |
happens-before 关系
volatile 写 happens-before 后续对同一变量的读。简单理解:线程 A 在写 volatile 变量之前做的普通写入,线程 B 读到这个 volatile 变量后,也能看到这些普通写入。
例如:
1 | private Map<String, Integer> latestStock; |
当查询线程读到 ready = true 后,可以看到 latestStock 的赋值。但这个写法仍要求 latestStock 后续不被并发修改。更稳的做法是用不可变 Map 或整体替换引用。
小结
volatile 适合表达“状态变化要被其他线程看到”。它不适合保护多个变量的一致性,也不适合做计数器自增。供应链系统里,任务停止标志、配置引用、只写一次的发布状态可以使用 volatile;库存扣减、单据状态变更、计数统计要用锁、原子类或数据库条件更新。