Java内存模型(JMM)

Java内存模型(Java Memory Model, JMM)是Java并发编程的核心基础,它定义了多线程环境下变量的访问规则,决定了在什么时候一个线程对共享变量的修改对其他线程可见。本文将全面剖析JMM的各个方面,从基础概念到高级应用,帮助开发者彻底理解并正确运用这一关键机制。

一、JMM概述:为什么需要内存模型?

1.1 计算机体系结构的内存问题

现代计算机体系结构中存在多个层次的内存访问优化,这导致了内存访问的一些特殊现象:

  • CPU缓存架构:多级缓存(L1/L2/L3)的存在导致内存可见性问题

  • 指令重排序:编译器/处理器为了优化性能可能改变指令执行顺序

  • 多核并发:不同CPU核心可能看到不同的内存状态

java 复制代码
// 典型的内存可见性问题示例
public class VisibilityProblem {
    private static boolean ready = false;
    private static int number = 0;
    
    public static void main(String[] args) {
        new Thread(() -> {
            while (!ready) {
                // 可能永远循环!
            }
            System.out.println(number);
        }).start();
        
        number = 42;
        ready = true;
    }
}

1.2 JMM的作用与意义

Java内存模型的主要目标:

  • 定义规则:规定线程如何与内存交互

  • 保证可见性:确保一个线程的修改对其他线程可见

  • 禁止重排序:限制编译器和处理器的优化行为

  • 提供同步机制:定义happens-before关系

二、JMM核心概念详解

2.1 主内存与工作内存

JMM将内存抽象为两种:

  • 主内存(Main Memory):所有共享变量的存储位置

  • 工作内存(Working Memory):每个线程私有的内存空间

变量访问流程

  1. 线程从主内存拷贝变量到工作内存

  2. 线程在工作内存中操作变量

  3. 在某个时刻将工作内存的值刷新回主内存

2.2 内存间交互操作

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

操作 作用
lock(锁定) 作用于主内存,标识变量为线程独占
unlock(解锁) 释放锁定状态
read(读取) 从主内存传输变量到工作内存
load(载入) 把read得到的值放入工作内存副本
use(使用) 把工作内存值传递给执行引擎
assign(赋值) 将执行引擎接收的值赋给工作内存变量
store(存储) 把工作内存值传送到主内存
write(写入) 把store得到的值放入主内存变量

操作规则

  • read/load和store/write必须成对出现

  • 不允许一个变量从主内存读取但工作内存不接受,或工作内存发起回写但主内存不接受

  • 新变量只能在主内存"诞生"

  • 一个变量同一时刻只允许一个线程lock

  • 对变量执行lock操作会清空工作内存中此变量的值

  • unlock前必须先把变量同步回主内存

2.3 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于其他线程检测到该线程已经终止

  7. 中断规则:一个线程调用另一个线程的interrupt happens-before于被中断线程检测到中断

  8. 终结器规则:对象的构造函数执行happens-before于它的finalize方法

java 复制代码
// happens-before示例
public class HappensBeforeExample {
    private int x = 0;
    private volatile boolean v = false;
    
    public void writer() {
        x = 42;  // (1)
        v = true; // (2) volatile写
    }
    
    public void reader() {
        if (v) {  // (3) volatile读
            System.out.println(x); // (4) 保证输出42
        }
    }
}

三、volatile关键字深度解析

3.1 volatile的语义

volatile变量具有两种特性:

  1. 可见性:对volatile变量的写操作会立即刷新到主内存

  2. 禁止重排序:编译器/处理器不会对volatile操作重排序

内存屏障

  • 写volatile变量时:在写操作前插入StoreStore屏障,写操作后插入StoreLoad屏障

  • 读volatile变量时:在读操作前插入LoadLoad屏障,读操作后插入LoadStore屏障

3.2 volatile与普通变量的区别

特性 普通变量 volatile变量
可见性 不保证 保证
原子性 32位以下基本类型具有原子性 任何读写操作都具有原子性
重排序 允许 限制
性能 较低(因为内存屏障)

3.3 volatile的正确使用场景

  1. 状态标志

    java 复制代码
    public class ShutdownRequest extends Thread {
        private volatile boolean shutdownRequested = false;
        
        public void shutdown() { shutdownRequested = true; }
        
        @Override
        public void run() {
            while (!shutdownRequested) {
                // 执行任务
            }
        }
    }
  2. 一次性安全发布

    java 复制代码
    public class Singleton {
        private volatile static Singleton instance;
        
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
  3. 独立观察

java 复制代码
public class UserManager {
    private volatile String lastUser;
    
    public void authenticate(String user, String password) {
        boolean valid = // 验证逻辑
        if (valid) {
            lastUser = user;
        }
    }
}

四、synchronized与锁的内存语义

4.1 synchronized的三大特性

  1. 原子性:确保互斥执行临界区代码

  2. 可见性:解锁前必须将变量刷新到主内存

  3. 有序性:限制临界区内指令重排序

4.2 锁的内存语义

  • 获取锁:清空工作内存,从主内存重新加载变量

  • 释放锁:将工作内存中的变量刷新到主内存

java 复制代码
public class SynchronizedExample {
    private int value = 0;
    
    public synchronized void increment() {
        value++; // 原子性+可见性保证
    }
    
    public int getValue() {
        synchronized(this) {
            return value; // 保证读取最新值
        }
    }
}

4.3 锁与volatile的比较

特性 synchronized volatile
原子性 保证代码块/方法级别的原子性 保证单个读/写的原子性
可见性 保证 保证
阻塞
适用范围 代码块/方法 变量
编译器优化 限制 限制
性能 较高开销 较低开销

五、final域的内存语义

5.1 final域的重排序规则

  1. 写final域:禁止把final域的写重排序到构造函数之外

  2. 读final域:初次读包含final域的对象引用时,保证final域已被初始化

5.2 final的正确使用

java 复制代码
public class FinalExample {
    private final int x;
    private int y;
    private static FinalExample instance;
    
    public FinalExample() {
        x = 1;  // final写
        y = 2;  // 普通写
    }
    
    public static void writer() {
        instance = new FinalExample();
    }
    
    public static void reader() {
        FinalExample object = instance;
        int a = object.x; // 保证读到1
        int b = object.y; // 可能读到0
    }
}

5.3 final与线程安全

正确构造的不可变对象是线程安全的:

java 复制代码
public class ImmutablePoint {
    private final int x;
    private final int y;
    
    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
    // 只有getter方法
}

六、双重检查锁定问题与解决方案

6.1 错误的双重检查锁定

java 复制代码
public class DoubleCheckedLocking {
    private static Instance instance;
    
    public static Instance getInstance() {
        if (instance == null) {                  // 第一次检查
            synchronized (DoubleCheckedLocking.class) {
                if (instance == null) {          // 第二次检查
                    instance = new Instance();   // 问题根源!
                }
            }
        }
        return instance;
    }
}

问题根源instance = new Instance()可能被重排序为:

  1. 分配内存空间

  2. 将引用指向内存空间(此时instance!=null)

  3. 初始化对象

6.2 正确的解决方案

  1. 使用volatile

    java 复制代码
    private volatile static Instance instance;
  2. 静态内部类

    java 复制代码
    public class Singleton {
        private static class Holder {
            static final Instance INSTANCE = new Instance();
        }
        
        public static Instance getInstance() {
            return Holder.INSTANCE;
        }
    }
  3. 枚举实现

    java 复制代码
    public enum Singleton {
        INSTANCE;
        
        public void someMethod() { ... }
    }

七、JMM与并发工具类

7.1 Atomic类的内存语义

原子类基于CAS实现,具有volatile读写的内存语义:

java 复制代码
public class AtomicExample {
    private final AtomicInteger counter = new AtomicInteger(0);
    
    public void increment() {
        counter.incrementAndGet(); // 包含内存屏障
    }
    
    public int get() {
        return counter.get(); // 具有volatile读语义
    }
}

7.2 ConcurrentHashMap的内存保证

ConcurrentHashMap通过分段锁和volatile变量保证内存可见性:

java 复制代码
static final class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;         // 值用volatile修饰
    volatile Node<K,V> next; // 下一个节点也用volatile修饰
}

7.3 CountDownLatch的内存语义

java 复制代码
public class CountDownLatchDemo {
    private final CountDownLatch latch = new CountDownLatch(1);
    private int result;
    
    public void compute() {
        new Thread(() -> {
            result = doCompute(); // (1)
            latch.countDown();    // (2) 释放存储屏障
        }).start();
    }
    
    public int getResult() throws InterruptedException {
        latch.await();           // (3) 获取加载屏障
        return result;           // (4) 保证看到(1)的结果
    }
}

八、JMM实战问题与解决方案

8.1 伪共享(False Sharing)问题

问题现象

  • 多个线程频繁修改同一缓存行的不同变量

  • 导致缓存行无效,性能下降

解决方案

  1. 填充(Padding)

    java 复制代码
    public class FalseSharing {
        public volatile long value1;
        public long p1, p2, p3, p4, p5, p6, p7; // 填充
        public volatile long value2;
    }
  2. 使用@Contended注解(Java8+)

    java 复制代码
    public class FalseSharing {
        @jdk.internal.vm.annotation.Contended
        public volatile long value1;
        
        @jdk.internal.vm.annotation.Contended
        public volatile long value2;
    }

8.2 安全发布模式

  1. 静态初始化

    java 复制代码
    public static Holder holder = new Holder(42);
  2. volatile或AtomicReference

    java 复制代码
    public volatile Holder holder;
  3. final域

    java 复制代码
    public class HolderWrapper {
        public final Holder holder;
        public HolderWrapper(Holder holder) {
            this.holder = holder;
        }
    }

九、JMM与Java新特性

9.1 Java 9+的VarHandle

提供更灵活的内存访问操作:

java 复制代码
public class VarHandleExample {
    private int x;
    private static final VarHandle X;
    
    static {
        try {
            X = MethodHandles.lookup()
                .findVarHandle(VarHandleExample.class, "x", int.class);
        } catch (Exception e) {
            throw new Error(e);
        }
    }
    
    public void increment() {
        X.getAndAdd(this, 1); // 原子操作
    }
}

十、JMM最佳实践

    1. 优先使用高层并发工具

      • 使用ConcurrentHashMap而不是同步的HashMap

      • 使用AtomicLong而不是synchronized计数器

    2. 最小化同步范围

      • 同步块比同步方法更好

      • 只在必要时使用同步

    3. 正确发布共享对象

      • 使用final字段

      • 使用volatile或正确同步

    4. 避免过度同步

      • 考虑使用不可变对象

      • 使用线程封闭技术(ThreadLocal)

    5. 理解happens-before关系

      • 不要依赖执行时序

      • 确保正确的同步

    6. 性能考虑

      • 测量而不是猜测

      • 注意伪共享问题

      • 考虑无锁算法

相关推荐
不太厉害的程序员7 分钟前
eclipse更改jdk环境和生成webservice客户端代码
java·ide·后端·eclipse·webservice
悟能不能悟9 分钟前
ode with me是idea中用来干嘛的插件
java·ide·intellij-idea
Murphy_lx9 分钟前
C++多态的原理
java·开发语言·c++
神仙别闹12 分钟前
基于JSP+MySQL 实现(Web)毕业设计题目收集系统
java·前端·mysql
小韩学长yyds27 分钟前
大疆无人机开发:MQTT 赋能机场系统集成的Java实战之旅
java·无人机
考虑考虑30 分钟前
JDK21中的虚拟线程
java·后端·java ee
AI大模型1 小时前
35岁程序员的出路:AI赛道疯狂抢人,年薪百万不是梦
java·程序员·llm
程序员编程指南1 小时前
Qt 移动应用发布与分发指南
c语言·开发语言·c++·qt
工程师0072 小时前
C#反射的概念与实战
开发语言·c#·反射
程序员编程指南2 小时前
Qt 与物联网(IoT)开发
c语言·开发语言·c++·qt·物联网