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()包含三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存地址

如果没有volatile,步骤2和3可能重排序,导致其他线程看到未完全初始化的对象。

(五) happens-before规则实战

1. 程序次序规则

int x = 10;     // 1
int y = 20;     // 2  happens-before 3  
int z = x + y;  // 3

2. 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内存模型,再到具体的内存屏障实现,这是一个层层递进的知识体系。

版权声明 ▶ 本网站名称:黄磊的博客
▶ 本文标题:JVM内存模型深度解析:从硬件架构到Java内存模型的完整视角
▶ 本文链接:https://www.huangleicole.com/backend-related/37.html
▶ 转载本站文章需要遵守:商业转载请联系站长,非商业转载请注明出处!!

如果觉得我的文章对你有用,请随意赞赏