1. 为什么每个Java开发者都必须理解JMM
第一次用volatile解决可见性问题时,那种"原来如此"的顿悟感至今难忘。当时在电商促销活动中,商品库存明明已经扣减,但其他线程仍然看到旧值,导致出现了超卖。这个看似简单的bug让我连续加班三天,最终发现根本原因在于对Java内存模型(JMM)的理解不足。
JMM是Java并发编程的基石模型,它定义了线程如何与内存交互。不同于物理机的内存架构,JMM抽象出一套自己的规则,这使得:
- 编写代码时不必考虑具体CPU架构
- 但必须严格遵守JMM的约定,否则就会遇到诡异的并发问题
在实际业务场景中,我见过太多因为不理解JMM导致的典型问题:
- 配置中心参数已更新但服务仍读取旧值(可见性问题)
- 支付状态判断成功却重复入账(原子性问题)
- 对象初始化未完成就被其他线程使用(有序性问题)
2. JMM核心架构解析
2.1 工作内存与主内存的二分世界
JMM将内存系统抽象为两个层级:
- 主内存(Main Memory):存储所有共享变量
- 工作内存(Work Memory):每个线程私有的存储空间
当线程操作共享变量时,实际发生的是:
- 从主内存拷贝副本到工作内存(read)
- 在工作内存修改副本(load/use/assign)
- 将修改后的值写回主内存(store/write)
这种设计带来了性能提升,但也引入了数据一致性问题。比如在秒杀系统中:
// 问题代码示例 public class Inventory { private int stock = 100; public void deduct() { if(stock > 0) { stock--; // 非原子操作 } } }每个线程的工作内存中可能都有stock的副本,导致超卖。
2.2 重排序的优化与风险
现代处理器和编译器会进行指令重排序优化,JMM规定了几种不会改变单线程执行结果的重排序规则。但在多线程环境下,这可能导致意想不到的结果:
// 经典的双重检查锁定问题 public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // 问题出在这里 } } } return instance; } }对象的初始化操作可能被重排序,导致其他线程获取到未初始化完成的对象。
3. 并发三大问题的本质与解决方案
3.1 可见性问题:volatile的正确打开方式
可见性问题指一个线程的修改对其他线程不可见。volatile通过以下机制保证可见性:
- 写操作后会立即刷新到主内存
- 读操作前会从主内存重新加载
适合使用volatile的场景:
- 状态标志位(如shutdown请求)
- 一次性安全发布(如双重检查锁定中的实例引用)
重要提示:volatile不能保证复合操作的原子性,比如i++这种操作
3.2 原子性问题:锁与CAS的抉择
原子性问题的解决方案对比:
| 方案 | 实现方式 | 适用场景 | 性能影响 |
|---|---|---|---|
| synchronized | 悲观锁 | 高竞争场景 | 较大 |
| ReentrantLock | 可重入锁 | 需要高级功能时 | 中等 |
| AtomicXXX | CAS操作 | 低竞争场景 | 最小 |
在订单系统中处理库存扣减的推荐做法:
// 使用AtomicInteger的CAS实现 private AtomicInteger stock = new AtomicInteger(100); public boolean deduct() { int current; do { current = stock.get(); if(current <= 0) { return false; } } while(!stock.compareAndSet(current, current - 1)); return true; }3.3 有序性问题:happens-before原则实战
JMM通过happens-before规则定义操作间的可见性关系,关键规则包括:
- 程序顺序规则
- 锁规则
- volatile变量规则
- 线程启动规则
- 传递性
在开发中可以利用这些规则避免同步开销。例如实现高效的对象发布:
public class ResourceHolder { private static class Holder { static final Resource INSTANCE = new Resource(); } public static Resource getInstance() { return Holder.INSTANCE; // 利用类初始化保证线程安全 } }4. 高并发系统设计实践
4.1 抢单系统设计要点
结合JMM原理设计抢单系统时需要注意:
- 库存扣减必须保证原子性(建议用Redis+Lua或分布式锁)
- 订单状态变更需要保证可见性(使用volatile或显式同步)
- 创建订单操作需要保证有序性(防止指令重排序)
典型实现架构:
用户请求 → 限流层 → 库存预扣减 → 订单创建 → 支付处理 ↑ ↑ 缓存击穿保护 分布式锁控制4.2 JVM内存模型优化技巧
- 减少共享变量:能用局部变量就不用成员变量
- 缩小同步范围:同步块尽可能小
- 使用final字段:天然线程安全
- 避免过早优化:先保证正确性再考虑性能
在Spring Boot应用中可以通过以下配置优化:
# 设置合适的堆大小避免频繁GC -server -Xms2g -Xmx2g # 使用G1垃圾收集器减少停顿 -XX:+UseG1GC # 关闭偏向锁减少同步开销 -XX:-UseBiasedLocking5. 常见陷阱与排查指南
5.1 伪共享问题识别与解决
伪共享(False Sharing)是性能杀手,可以通过@Contended注解或字段填充解决:
// JDK8+的解决方案 public class Counter { @sun.misc.Contended public volatile long value1 = 0L; @sun.misc.Contended public volatile long value2 = 0L; }5.2 死锁排查实战
使用jstack检测死锁的步骤:
jps -l查找Java进程IDjstack <pid> > thread_dump.txt- 搜索"deadlock"关键词
- 分析锁持有和等待关系
预防死锁的建议:
- 按固定顺序获取锁
- 使用tryLock设置超时
- 避免在同步块中调用外部方法
6. 性能优化实测数据
在不同并发级别下的性能对比(测试环境:4核8G,JDK11):
| 并发线程数 | synchronized | ReentrantLock | CAS |
|---|---|---|---|
| 10 | 12,000 TPS | 15,000 TPS | 18,000 TPS |
| 50 | 8,000 TPS | 12,000 TPS | 16,000 TPS |
| 100 | 5,000 TPS | 9,000 TPS | 14,000 TPS |
关键发现:
- 低竞争时CAS优势明显
- 高竞争时ReentrantLock的吞吐量更稳定
- synchronized在JDK6+经过优化后性能差距缩小
7. 新版JDK中的改进
JDK15引入的ZGC在内存模型方面的优化:
- 染色指针技术减少内存屏障使用
- 可配置的本地内存分配
- 亚毫秒级停顿时间
示例启动参数:
java -XX:+UseZGC -Xmx16g -XX:ConcGCThreads=4 MyApp8. 个人踩坑经验
- 不要过度依赖volatile,它不能替代锁
- ConcurrentHashMap的size()方法结果不一定精确
- ThreadLocal使用后必须remove避免内存泄漏
- 异步编程时注意上下文传递问题
在微服务环境中特别要注意:
// 错误的线程池使用方式 @Bean public Executor taskExecutor() { return Executors.newCachedThreadPool(); // 可能导致OOM } // 推荐方式 @Bean public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(100); executor.setThreadNamePrefix("Async-"); executor.initialize(); return executor; }