AI摘要
文章从并发bug切入,剖析CPU缓存与MESI协议,系统梳理JVM内存模型、volatile/synchronized内存语义、四种内存屏障及happens-before规则,并给出DCL、手写屏障等实战示例,构建硬件到Java的完整并发内存观。
(一) 从一次诡异的并发bug说起
我们有一个计数器服务,在8核服务器上运行,理论上性能应该很好,但实际TPS却上不去。查看代码:
@Service
public class CounterService {
private long count = 0;
public void increment() {
count++; // 这行代码有隐患!
}
public long getCount() {
return count;
}
}问题分析: count++不是原子操作,在并发环境下会出现数据竞争。但更深入的问题是内存可见性。
(二) 现代计算机的内存架构
1. CPU缓存架构
CPU Core1 → L1 Cache → L2 Cache → L3 Cache → 主内存
CPU Core2 → L1 Cache → L2 Cache → L3 Cache → 主内存每个CPU核心有自己的缓存,这导致了缓存一致性问题。
2. MESI缓存一致性协议
- Modified:缓存行被修改,与主内存不一致
- Exclusive:缓存行独占,与主内存一致
- Shared:缓存行被多个CPU共享
- Invalid:缓存行无效
当Core1修改数据时,需要将其他Core的对应缓存行标记为Invalid。
(三) Java内存模型(JMM)详解
1. 主内存与工作内存
线程工作内存 → 保存该线程使用变量的副本
↓ load、read、use、assign、store、write
主内存 → 存储所有共享变量2. volatile的内存语义
public class VolatileExample {
private volatile boolean flag = false;
private int number = 0;
public void writer() {
number = 42; // 普通写
flag = true; // volatile写
}
public void reader() {
if (flag) { // volatile读
System.out.println(number); // 保证看到42
}
}
}olatile的底层实现:
- 写操作:在指令后加入
lock前缀,将缓存写回内存,并使其他CPU缓存失效 - 读操作:从主内存重新加载数据
3. synchronized的内存语义
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++; // 临界区代码
}
public void incrementWithLock() {
// 手动模拟synchronized的锁膨胀过程
// 偏向锁 → 轻量级锁 → 重量级锁
}
}(四) 内存屏障(Memory Barrier)深度解析
1. 四种内存屏障
public class MemoryBarrierExample {
private int a, b;
private volatile int v;
public void test() {
// StoreStore屏障:保证普通写在前,volatile写在后
a = 1; // 普通写
// StoreStore屏障(编译器插入)
v = 2; // volatile写
// LoadLoad屏障:保证volatile读在前,普通读在后
int localV = v; // volatile读
// LoadLoad屏障(编译器插入)
int localB = b; // 普通读
// StoreLoad屏障:全能屏障,保证所有写操作对所有处理器可见
// LoadStore屏障:保证读操作在写操作之前完成
}
}2. 实际案例:DCL(双检锁)的完整版本
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) { // 第一次检查
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null) { // 第二次检查
instance = new Instance(); // 问题所在!
}
}
}
return instance;
}
}为什么需要volatile?
instance = new Instance()包含三个步骤:
- 分配内存空间
- 初始化对象
- 将引用指向内存地址
如果没有volatile,步骤2和3可能重排序,导致其他线程看到未完全初始化的对象。
(五) happens-before规则实战
1. 程序次序规则
int x = 10; // 1
int y = 20; // 2 happens-before 3
int z = x + y; // 32. volatile变量规则
// 线程1
sharedVariable = 42; // 写操作
volatileFlag = true; // volatile写
// 线程2
if (volatileFlag) { // volatile读
System.out.println(sharedVariable); // 保证看到42
}3. 传递性规则
// 线程1
x = 1;
synchronized (lock) {
y = 2; // 1 happens-before 2
} // 2 happens-before 3(监视器锁规则)
// 线程2
synchronized (lock) {
int a = y; // 3 happens-before 4
z = 3; // 4 happens-before 5
} // 5 happens-before 6(监视器锁规则)
// 线程3
int b = z; // 保证看到3
System.out.println(x); // 保证看到1(传递性)(六) 实战:手写一个简单的内存屏障
public class UnsafeUtils {
private static final Unsafe UNSAFE;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE = (Unsafe) field.get(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 模拟volatile写的内存屏障效果
public static void fullFence() {
UNSAFE.fullFence();
}
public static void loadFence() {
UNSAFE.loadFence();
}
public static void storeFence() {
UNSAFE.storeFence();
}
}
// 使用示例
public class ManualMemoryBarrier {
private int data;
private boolean ready;
public void publish() {
data = 42;
UnsafeUtils.storeFence(); // 保证data写入在ready之前对其它线程可见
ready = true;
}
public void consume() {
if (ready) {
UnsafeUtils.loadFence(); // 保证ready读取在data之前
System.out.println(data); // 保证看到42
}
}
}总结: 理解JVM内存模型是编写正确并发程序的基础。从硬件缓存一致性到Java内存模型,再到具体的内存屏障实现,这是一个层层递进的知识体系。