1. 为什么我们需要重新认识Java内存模型?
记得刚入行时,我遇到过一个诡异的并发问题:两个线程交替修改同一个boolean标志位,明明已经用volatile修饰了变量,但程序还是偶尔会卡死。当时花了整整三天才搞明白,原来是我对Java内存模型(JMM)的理解存在根本性缺陷。今天我们就来彻底拆解JMM的三大核心支柱,让你从根源上理解Java并发问题的本质。
JMM绝不只是volatile和synchronized那么简单,它定义了线程如何以及何时可以看到其他线程写入的共享变量值,以及在必要时如何同步访问这些变量。理解JMM能帮你:
- 准确诊断那些"见鬼了"的并发问题
- 写出真正线程安全的代码,而不只是碰运气
- 在面试中碾压式回答所有Java并发问题
- 设计出更高性能的并发架构
2. JMM三大核心支柱深度解析
2.1 可见性问题:不只是volatile那么简单
可见性问题源于现代计算机的多级缓存架构。来看这个经典案例:
java复制class VisibilityDemo {
boolean ready = false; // 非volatile
void writer() {
ready = true; // 操作1
}
void reader() {
while (!ready); // 操作2
System.out.println("可见了!");
}
}
即使writer线程先执行操作1,reader线程也可能永远看不到ready的变化。这是因为:
- 每个CPU核心有自己的缓存
- 非volatile变量修改可能只写入缓存而不立即刷回主存
- 其他线程读取时可能直接从主存获取旧值
关键认知:volatile不是解决可见性的唯一方式。synchronized块、final变量、Atomic类等都能建立happens-before关系保证可见性。
2.2 有序性问题:指令重排序的陷阱
现代处理器和编译器会进行指令重排序优化,看这个例子:
java复制class ReorderingDemo {
int x = 0;
boolean flag = false;
void writer() {
x = 42; // 操作1
flag = true; // 操作2
}
void reader() {
if (flag) { // 操作3
System.out.println(x); // 可能输出0!
}
}
}
即使没有多线程,单线程内也可能因为重排序导致操作2先于操作1执行。JMM通过happens-before规则约束这些重排序。
2.3 happens-before规则:JMM的基石
happens-before是理解JMM最关键的概念,它定义了操作间的可见性保证。重点规则包括:
- 程序顺序规则:同一线程内的操作按程序顺序happens-before
- volatile规则:volatile写happens-before后续任意读
- 锁规则:解锁happens-before后续加锁
- 传递性:如果A hb B,且B hb C,那么A hb C
3. 实战:如何正确应用JMM解决并发问题
3.1 volatile的正确使用姿势
volatile最适合的状态标志模式:
java复制class SafeShutdown {
volatile boolean shutdownRequested;
void shutdown() { shutdownRequested = true; }
void doWork() {
while (!shutdownRequested) {
// 执行任务
}
}
}
但volatile不能保证复合操作的原子性:
java复制// 错误用法!
volatile int count = 0;
count++; // 这不是原子操作!
3.2 同步块的深度优化
synchronized不仅提供互斥,还建立happens-before关系。现代JVM对锁进行了大量优化:
- 偏向锁:无竞争时直接进入
- 轻量级锁:通过CAS竞争
- 重量级锁:真正的阻塞同步
锁优化建议:
- 减小同步块范围
- 避免在同步块内调用耗时操作
- 考虑使用ReentrantLock的高级特性
3.3 原子类的底层原理
AtomicInteger等类使用CAS+volatile实现无锁线程安全:
java复制public class AtomicInteger {
private volatile int value;
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
}
CAS的ABA问题可以通过AtomicStampedReference解决。
4. JMM在常见框架中的应用
4.1 ConcurrentHashMap的并发设计
JDK8的ConcurrentHashMap放弃了分段锁,改用:
- Node数组+链表/红黑树
- CAS实现无锁化插入
- synchronized锁单个Node
4.2 ThreadLocal的内存泄漏防范
ThreadLocal的实现关键点:
java复制static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 弱引用Key
value = v;
}
}
}
最佳实践:
- 总是用static final修饰ThreadLocal实例
- 使用后及时调用remove()
4.3 线程池的状态控制
ThreadPoolExecutor用AtomicInteger同时保存:
- 线程数(低29位)
- 运行状态(高3位)
java复制private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
这种位操作技巧值得学习。
5. 高级话题:内存屏障与JVM实现
5.1 四种内存屏障类型
JVM在volatile访问时插入特定屏障:
- LoadLoad屏障:禁止读与读重排序
- StoreStore屏障:禁止写与写重排序
- LoadStore屏障:禁止读与写重排序
- StoreLoad屏障:禁止写与读重排序
5.2 JVM层面的实现差异
不同CPU架构的内存模型不同,JVM需要:
- x86:只需要StoreLoad屏障
- ARM:需要全部四种屏障
- 通过模板解释器和JIT编译器适配
6. 避坑指南:常见JMM误区和解决方案
6.1 双重检查锁的正确实现
经典错误实现:
java复制// 错误版本!
class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
正确实现需要volatile:
java复制private static volatile Singleton instance;
6.2 不可变对象的线程安全保证
正确利用final的happens-before规则:
java复制class ImmutableObject {
private final int x;
private final HashMap<String, String> map;
public ImmutableObject(int x, HashMap<String, String> map) {
this.x = x;
this.map = new HashMap<>(map); // 防御性拷贝
}
}
6.3 避免过度同步的性能陷阱
同步优化对比:
java复制// 过度同步
synchronized void process(List<Data> list) {
for (Data d : list) {
// 处理逻辑
}
}
// 优化版本
void process(List<Data> list) {
List<Data> copy;
synchronized(this) {
copy = new ArrayList<>(list);
}
for (Data d : copy) {
// 处理逻辑
}
}
7. 终极测试:你的JMM知识达标了吗?
用这个例子检验你的理解:
java复制class Puzzle {
int a = 0;
int b = 0;
void thread1() {
a = 1;
synchronized(this) {
b = 1;
}
}
void thread2() {
synchronized(this) {
System.out.println(b);
}
System.out.println(a);
}
}
问题:thread2可能输出什么结果?为什么?
真正掌握JMM后,你会发现Java并发问题不再神秘。我在处理分布式系统时,这些基础知识每天都会用到。记住:理解happens-before关系,是写出正确并发程序的关键。