Java内存模型(JMM)与多线程并发机制深度解析

一、Java内存模型(JMM)基础

1.1 JMM核心概念

Java Memory Model(JMM)定义了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量的底层细节。

关键特性

  • 所有变量都存储在主内存中
  • 每个线程有自己的工作内存(缓存)
  • 线程对变量的所有操作都必须在工作内存中进行
  • 不同线程之间无法直接访问对方工作内存中的变量

图1

1.2 主内存与工作内存交互

JMM定义了8种原子操作来完成主内存与工作内存的交互:

  1. lock(锁定):作用于主内存变量
  2. unlock(解锁):作用于主内存变量
  3. read(读取):从主内存传输到工作内存
  4. load(载入):将read得到的值放入工作内存变量副本
  5. use(使用):将工作内存变量值传递给执行引擎
  6. assign(赋值):将执行引擎接收的值赋给工作内存变量
  7. store(存储):将工作内存变量值传送到主内存
  8. write(写入):将store得到的值放入主内存变量

实践建议

  • 理解这些原子操作有助于分析多线程环境下的可见性问题
  • 在编写高并发程序时,要特别注意这些操作的顺序和组合

二、happens-before原则

happens-before是JMM的核心概念,用于判断数据是否存在竞争、线程是否安全。

基本原则

  1. 程序顺序规则:同一线程中的每个操作happens-before于该线程中的任意后续操作
  2. 监视器锁规则:对一个锁的解锁happens-before于随后对这个锁的加锁
  3. volatile变量规则:对一个volatile域的写happens-before于任意后续对这个volatile域的读
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
  5. 线程启动规则:Thread对象的start()方法happens-before此线程的每一个动作
  6. 线程终止规则:线程中的所有操作都happens-before于对此线程的终止检测

示例代码

// 示例1:volatile的happens-before关系
class VolatileExample {
    int x = 0;
    volatile boolean v = false;
    
    public void writer() {
        x = 42;      // 1
        v = true;    // 2
    }
    
    public void reader() {
        if (v) {      // 3
            System.out.println(x);  // 4
        }
    }
}

实践建议

  • 合理利用happens-before原则可以减少同步开销
  • 在代码注释中明确happens-before关系,便于维护

三、内存屏障

内存屏障(Memory Barrier)是CPU或编译器对内存操作顺序的约束,用于保证特定操作的内存可见性。

主要类型

  1. LoadLoad屏障:序列:Load1; LoadLoad; Load2
  2. StoreStore屏障:序列:Store1; StoreStore; Store2
  3. LoadStore屏障:序列:Load1; LoadStore; Store2
  4. StoreLoad屏障:序列:Store1; StoreLoad; Load2

JVM中的实现

// 伪代码展示内存屏障使用
public class MemoryBarrierExample {
    private int value;
    private volatile boolean flag;
    
    public void setValue(int newValue) {
        value = newValue;          // 普通写
        // StoreStore屏障
        flag = true;              // volatile写
    }
    
    public int getValue() {
        if (flag) {               // volatile读
            // LoadLoad屏障 + LoadStore屏障
            return value;          // 普通读
        }
        return -1;
    }
}

实践建议

  • 理解内存屏障有助于优化高性能并发代码
  • 大多数情况下应使用高级同步机制而非直接操作内存屏障

四、线程实现与调度

4.1 线程状态转换

图2

4.2 协程(Loom项目)

Java 19引入的虚拟线程(协程)特性:

与传统线程对比

  1. 轻量级:一个JVM线程可承载数千个虚拟线程
  2. 低开销:上下文切换由JVM管理而非操作系统
  3. 简化并发编程:使用同步代码风格实现异步效果

示例代码

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}  // executor.close()会等待所有任务完成

实践建议

  • 对于I/O密集型应用,虚拟线程可以显著提高吞吐量
  • 计算密集型任务仍需使用平台线程

五、同步机制深度解析

5.1 synchronized实现原理

对象头结构

|-------------------------------------------------------|
| Mark Word (64 bits)                   | Klass Word (64 bits) |
|-------------------------------------------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |     OOP to metadata object    |
|-------------------------------------------------------|

锁升级过程

  1. 无锁状态
  2. 偏向锁:通过CAS设置ThreadID
  3. 轻量级锁:通过CAS自旋尝试获取锁
  4. 重量级锁:向操作系统申请互斥量

图3

5.2 AQS框架原理

AbstractQueuedSynchronizer是Java并发包的核心基础组件。

核心结构

// 简化的AQS核心结构
public abstract class AbstractQueuedSynchronizer {
    private volatile int state; // 同步状态
    private transient volatile Node head; // CLH队列头
    private transient volatile Node tail; // CLH队列尾
    
    static final class Node {
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
    }
    
    protected final boolean compareAndSetState(int expect, int update) {
        // CAS操作更新state
    }
}

实践建议

  • 理解AQS有助于自定义高性能同步器
  • 大多数情况下应使用Java并发包提供的现成工具类

六、锁优化策略

6.1 常见优化技术

  1. 锁消除:JIT编译器通过逃逸分析移除不可能存在竞争的锁

    public String concat(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }
  2. 锁粗化:将连续的锁请求合并为一个

    // 优化前
    for (int i = 0; i < 100; i++) {
        synchronized(lock) {
            // do something
        }
    }
    
    // 优化后
    synchronized(lock) {
        for (int i = 0; i < 100; i++) {
            // do something
        }
    }
  3. 适应性自旋:根据历史成功率动态调整自旋次数

6.2 实践建议

  1. 优先使用java.util.concurrent包中的并发工具
  2. 减小锁粒度(如ConcurrentHashMap的分段锁)
  3. 读写分离(使用ReadWriteLock)
  4. 考虑使用无锁数据结构(如Atomic类)
  5. 监控锁竞争情况(jstack, JMC等工具)

七、总结与最佳实践

  1. 内存可见性

    • 使用volatile保证单个变量的可见性
    • 使用synchronized或Lock保证代码块的可见性
  2. 线程安全设计

    // 线程安全计数器三种实现方式对比
    
    // 1. synchronized方法
    class Counter {
        private int value;
        public synchronized int increment() {
            return ++value;
        }
    }
    
    // 2. ReentrantLock
    class Counter {
        private int value;
        private final Lock lock = new ReentrantLock();
        public int increment() {
            lock.lock();
            try {
                return ++value;
            } finally {
                lock.unlock();
            }
        }
    }
    
    // 3. AtomicInteger (最优)
    class Counter {
        private final AtomicInteger value = new AtomicInteger();
        public int increment() {
            return value.incrementAndGet();
        }
    }
  3. 性能考量

    • 低竞争场景:偏向锁/轻量级锁
    • 中等竞争:适当自旋+CAS
    • 高竞争:减少临界区大小或使用无锁算法
  4. 调试技巧

    # 查看锁竞争情况
    jstack <pid> | grep -A 10 " contended "
    
    # 监控线程状态
    jcmd <pid> Thread.print

通过深入理解JMM和多线程机制,开发者可以编写出既正确又高效的并发程序。随着Java虚拟线程的成熟,并发编程模型还将继续演进,值得持续关注。

添加新评论