Java内存模型(JMM)开发应用

 更新时间:2025年11月04日 08:47:48   作者:佛祖让我来巡山  
JMM既是一个规范也是一个工具,正确理解和使用JMM可以帮助我们编写出既正确又高效的多线程程序,接下来通过本文给大家介绍Java内存模型(JMM)开发应用实践记录,感兴趣的朋友跟随小编一起看看吧

JMM核心内容概览与重要程度评级

在学习JMM前,我们先了解其核心内容体系及重要程度:

内容模块重要程度说明
1. JMM基础概念⭐⭐⭐⭐理解JMM的出发点和基本架构
- 硬件基础与并发挑战⭐⭐⭐⭐了解JMM存在的必要性
- 主内存与工作内存⭐⭐⭐⭐JMM的核心抽象概念
2. 内存间交互操作⭐⭐⭐JMM的基础操作定义
3. volatile关键字⭐⭐⭐⭐⭐最常用的同步机制,必须深入掌握
4. synchronized内存语义⭐⭐⭐⭐⭐理解锁的内存效应
5. happens-before规则⭐⭐⭐⭐⭐JMM的理论核心,解决可见性问题的关键
6. 原子性、可见性、有序性⭐⭐⭐⭐⭐并发编程的三大核心问题
7. 安全发布模式⭐⭐⭐⭐实际开发中的常用技巧
8. final字段语义⭐⭐⭐特殊但重要的内存语义
9. 双重检查锁定问题⭐⭐⭐⭐经典问题的分析与解决方案
10. JMM底层实现⭐⭐⭐理解原理,优化性能

接下来,我们将按照重要程度,逐一深入讲解各个模块。

1. JMM是什么?为什么需要JMM?

1.1 JMM的定义与作用

Java内存模型(Java Memory Model, JMM) 是Java虚拟机规范中定义的一种抽象规范,用于屏蔽各种硬件和操作系统的内存访问差异,实现Java程序在各种平台下都能达到一致的内存访问效果。

JMM的核心作用

  • 定义规则:规定多线程环境下变量的访问方式
  • 提供保证:确保在不同平台上内存访问行为的一致性
  • 允许优化:在保证正确性的前提下允许编译器和处理器进行优化

1.2 为什么需要JMM:硬件层面的挑战

现代计算机系统的多层次存储架构导致了并发编程的三大核心问题:

public class ConcurrencyProblems {
    private static boolean ready = false;
    private static int number = 0;
    public static void main(String[] args) {
        // 线程1:数据准备
        Thread writer = new Thread(() -> {
            number = 42;    // 操作1:可能被重排序到操作2之后
            ready = true;   // 操作2:可能先执行
        });
        // 线程2:数据处理
        Thread reader = new Thread(() -> {
            while (!ready) {
                // 等待ready变为true
                Thread.yield();
            }
            // 可能输出0而不是42!
            System.out.println("Number: " + number);
        });
        writer.start();
        reader.start();
    }
}

问题根源

  • CPU缓存一致性:多核CPU各有缓存,数据更新不同步
  • 指令重排序:编译器和处理器为优化性能重新排序指令
  • 内存可见性:一个线程的修改对其他线程不可见

2. JMM的核心架构:主内存与工作内存

JMM通过抽象的内存模型解决上述问题:

工作内存与主内存的交互通过8种原子操作完成

public class MemoryOperations {
    private int sharedValue = 0;
    public void operationExample() {
        // 1. read: 从主内存读取变量到传输通道
        // 2. load: 将read得到的值放入工作内存的变量副本
        // 相当于: int temp = sharedValue; (但这是高级语言表示)
        // 3. use: 将工作内存中的变量传递给执行引擎
        int result = sharedValue * 2;
        // 4. assign: 将执行引擎的结果赋给工作内存中的变量
        sharedValue = result + 1;
        // 5. store: 将工作内存中的变量值传输到主内存的传输通道
        // 6. write: 将store获取的值放入主内存的变量
        // 7. lock: 将主内存变量标记为线程独占状态
        // 8. unlock: 释放锁定的变量
    }
}

3. ⭐⭐⭐⭐⭐ volatile关键字深度解析

3.1 volatile的语义与保证

volatile是JVM提供的最轻量级的同步机制,提供两大保证:

  • 可见性保证:对volatile变量的写操作立即对其他线程可见
  • 禁止重排序:阻止编译器和处理器对volatile操作进行重排序
public class VolatileExample {
    private volatile boolean flag = false;
    private int value = 0;
    public void writer() {
        value = 42;      // 普通写操作
        // StoreStore内存屏障:禁止上面的普通写与下面的volatile写重排序
        flag = true;     // volatile写操作
        // StoreLoad内存屏障:确保volatile写立即对其他处理器可见
    }
    public void reader() {
        // LoadLoad内存屏障:确保volatile读之前的所有读操作已完成
        if (flag) {      // volatile读操作
            // LoadStore内存屏障:确保volatile读之后的写操作不会重排序到读之前
            System.out.println(value); // 保证看到value = 42
        }
    }
}

3.2 volatile的实现原理

在硬件层面,volatile通过内存屏障指令实现:

public class VolatileBarrier {
    private volatile int value;
    public void setValue(int newValue) {
        this.value = newValue;
        // 对应x86汇编代码:
        // mov    %eax,0x10(%rsi)   ; 将newValue存入value的内存地址
        // lock addl $0x0,(%rsp)    ; StoreLoad内存屏障(mfence指令)
    }
    public int getValue() {
        // volatile读在x86上不需要特殊指令
        // 因为x86的内存模型已经保证了可见性(TSO模型)
        return value;
    }
}

内存屏障类型

  • LoadLoad屏障:禁止读操作重排序
  • StoreStore屏障:禁止写操作重排序
  • LoadStore屏障:禁止读与写操作重排序
  • StoreLoad屏障:禁止写与读操作重排序(最重量级)

3.3 volatile的使用场景与限制

适用场景

  • 状态标志位
  • 一次性安全发布
  • 独立观察(independent observation)
  • 开销较低的读-写锁策略

不适用场景

  • 复合操作(如i++)
  • 依赖于当前值的操作(如value = value + 1)
public class VolatileUsage {
    // 场景1:状态标志位
    private volatile boolean shutdownRequested;
    public void shutdown() {
        shutdownRequested = true;
    }
    public void doWork() {
        while (!shutdownRequested) {
            // 执行工作任务
        }
    }
    // 场景2:一次性安全发布
    private volatile Resource resource;
    public Resource getResource() {
        if (resource == null) {
            synchronized(this) {
                if (resource == null) {
                    resource = new Resource(); // 安全发布
                }
            }
        }
        return resource;
    }
}

4. ⭐⭐⭐⭐⭐ synchronized的内存语义

synchronized不仅提供互斥执行,还提供重要的内存语义:

public class SynchronizedMemory {
    private int counter = 0;
    private final Object lock = new Object();
    public void increment() {
        synchronized(lock) { 
            // monitorenter指令:
            // 1. 清空工作内存
            // 2. 从主内存重新加载所有共享变量
            counter++;
            // 临界区内的操作不会被重排序到临界区外
        } 
        // monitorexit指令:
        // 1. 将工作内存中的修改刷新到主内存
        // 2. 释放锁
    }
    public int getCounter() {
        synchronized(lock) {
            // 获取锁会强制从主内存重新读取变量
            return counter;
        }
    }
}

synchronized的内存语义

  • 进入同步块:清空工作内存,从主内存重新加载变量
  • 退出同步块:将工作内存中的修改刷新到主内存
  • 互斥执行:确保同一时刻只有一个线程执行临界区代码

5. ⭐⭐⭐⭐⭐ happens-before规则

happens-before是JMM的理论核心,定义了操作之间的可见性关系。

5.1 happens-before规则详解

public class HappensBeforeExample {
    private int x = 0;
    private volatile boolean v = false;
    private int y = 0;
    private final Object lock = new Object();
    public void demo() {
        // 规则1:程序次序规则
        x = 1;          // 操作A
        v = true;       // 操作B:A happens-before B
        // 规则2:volatile变量规则
        if (v) {        // 操作C:B happens-before C
            y = x;      // 操作D:C happens-before D
        }
        // 规则3:传递性规则
        // A happens-before B, B happens-before C, C happens-before D
        // 因此 A happens-before D
        // 规则4:管程锁定规则
        synchronized(lock) { // 加锁E
            x = 2;          // 操作F:E happens-before F
        } // 解锁G:F happens-before G
        // 规则5:线程启动规则
        Thread t = new Thread(() -> {
            System.out.println(x); // 看到x=2
        });
        t.start(); // start() happens-before 线程中的所有操作
        // 规则6:线程终止规则
        try {
            t.join(); // 线程中的所有操作 happens-before join()返回
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        // 规则7:对象终结规则
        // 对象的构造函数执行结束 happens-before finalize()方法开始
    }
}

5.2 happens-before的数学基础

happens-before关系是一个偏序关系,具有:

  • 自反性:A happens-before A
  • 反对称性:如果A happens-before B且B happens-before A,则A=B
  • 传递性:如果A happens-before B且B happens-before C,则A happens-before C

6. ⭐⭐⭐⭐⭐ 原子性、可见性、有序性

这是并发编程的三大核心问题,JMM为每个问题提供了解决方案。

6.1 原子性(Atomicity)

原子性是指一个操作不可中断,要么全部执行成功,要么完全不执行。

public class AtomicityExample {
    private int basicType = 0; // 基本类型访问是原子的
    private long longValue = 0L; // long和double可能非原子(但现代JVM通常保证原子性)
    private volatile boolean flag = false; // volatile保证单个读/写的原子性
    // 复合操作不是原子的
    public void nonAtomicIncrement() {
        basicType++; // 不是原子操作!分解为read-modify-write三步
    }
    // 保证原子性的方式
    private final AtomicInteger atomicInt = new AtomicInteger(0);
    private final Object lock = new Object();
    private int synchronizedValue = 0;
    public void atomicOperations() {
        // 方式1:使用原子类
        atomicInt.incrementAndGet(); // 原子操作
        // 方式2:使用同步
        synchronized(lock) {
            synchronizedValue++; // 原子操作
        }
        // 方式3:使用volatile变量(仅适用于特定场景)
        flag = true; // 原子操作
    }
}

6.2 可见性(Visibility)

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

public class VisibilityExample {
    private int noVisibility = 0; // 无可见性保证
    private volatile boolean hasVisibility = false; // volatile保证可见性
    private int synchronizedValue = 0; // synchronized保证可见性
    private final Object lock = new Object();
    public void demonstrate() {
        // 线程1:修改数据
        new Thread(() -> {
            noVisibility = 42;
            hasVisibility = true;
            synchronized(lock) {
                synchronizedValue = 100;
            }
        }).start();
        // 线程2:读取数据
        new Thread(() -> {
            // 可能看不到noVisibility的更新
            while (!hasVisibility) {
                // 等待hasVisibility变为true
            }
            // 保证看到noVisibility = 42(因为volatile写happens-before volatile读)
            synchronized(lock) {
                // 保证看到synchronizedValue = 100
            }
        }).start();
    }
}

6.3 有序性(Ordering)

有序性是指程序执行的顺序按照代码的先后顺序执行。

public class OrderingExample {
    private int x = 0;
    private int y = 0;
    private volatile boolean ready = false;
    public void orderingDemo() {
        // 线程1:可能被重排序
        new Thread(() -> {
            x = 1;      // 操作1
            y = 2;      // 操作2
            ready = true; // 操作3:volatile写,阻止重排序
        }).start();
        // 线程2
        new Thread(() -> {
            while (!ready) {
                // 等待
            }
            // 由于volatile的语义,这里保证看到x=1, y=2
            // 不会出现y=2但x=0的情况
        }).start();
    }
}

7. ⭐⭐⭐⭐ 安全发布模式

安全地发布对象是并发编程中的常见需求,JMM提供了多种模式。

public class SafePublication {
    // 方式1:静态初始化器(最安全)
    private static final Resource staticResource = new Resource();
    // 方式2:volatile字段
    private volatile Resource volatileResource;
    public void initVolatileResource() {
        volatileResource = new Resource(); // 安全发布
    }
    // 方式3:final字段
    private final Resource finalResource;
    public SafePublication() {
        this.finalResource = new Resource(); // 安全发布
    }
    // 方式4:正常锁保护
    private Resource guardedResource;
    private final Object lock = new Object();
    public void initGuardedResource() {
        synchronized(lock) {
            if (guardedResource == null) {
                guardedResource = new Resource(); // 安全发布
            }
        }
    }
    // 方式5:线程安全容器
    private final Map<String, Resource> safeMap 
        = Collections.synchronizedMap(new HashMap<>());
    private final ConcurrentMap<String, Resource> concurrentMap 
        = new ConcurrentHashMap<>();
    public void addToSafeMap(String key) {
        safeMap.put(key, new Resource()); // 安全发布
    }
}

8. ⭐⭐⭐⭐ 双重检查锁定(DCL)问题与解决方案

双重检查锁定是一个经典的并发模式,但存在陷阱。

8.1 错误的DCL实现

public class BrokenDCL {
    private static Resource resource; // 没有volatile!
    public static Resource getInstance() {
        if (resource == null) {                 // 第一次检查(无锁)
            synchronized(BrokenDCL.class) {     // 加锁
                if (resource == null) {         // 第二次检查(有锁)
                    resource = new Resource();  // 问题所在!
                    // 可能发生的重排序:
                    // 1. 分配内存空间
                    // 2. 将引用指向内存空间(此时resource != null)
                    // 3. 初始化对象(还未执行)
                    // 其他线程可能拿到未完全初始化的对象!
                }
            }
        }
        return resource;
    }
}

8.2 正确的DCL实现

public class CorrectDCL {
    // 使用volatile禁止重排序
    private static volatile CorrectDCL instance;
    private final int value;
    private final String name;
    private CorrectDCL() {
        this.value = 42;     // 初始化final字段
        this.name = "DCL";   // 初始化普通字段
        // 构造函数执行
    }
    public static CorrectDCL getInstance() {
        if (instance == null) {                     // 第一次检查(无锁)
            synchronized(CorrectDCL.class) {        // 加锁
                if (instance == null) {             // 第二次检查(有锁)
                    instance = new CorrectDCL();    // 安全发布
                    // volatile写插入内存屏障,确保:
                    // 1. 所有初始化操作完成
                    // 2. 初始化结果对其他线程立即可见
                }
            }
        }
        return instance;
    }
}

9. ⭐⭐⭐ final字段的内存语义

final字段在并发编程中有特殊的内存语义,提供了安全初始化的保证。

public class FinalFieldExample {
    private final int finalValue; // final字段
    private int normalValue;      // 普通字段
    private volatile boolean ready = false;
    public FinalFieldExample() {
        normalValue = 1;        // 普通字段写入(可能被重排序)
        finalValue = 42;        // final字段写入
        // JMM在此隐式插入StoreStore内存屏障
        // 确保final字段的初始化不会被重排序到构造函数之外
        ready = true;           // volatile写
    }
    public static void reader() {
        FinalFieldExample obj = new FinalFieldExample();
        // 保证看到finalValue的正确值(42)
        // 即使没有同步,也能看到正确初始化的final字段
        int r1 = obj.finalValue;
        // 可能看到normalValue的默认值(0)而不是1
        // 因为没有同步保证
        int r2 = obj.normalValue;
        // 但如果通过volatile读看到ready=true
        // 那么也能保证看到所有字段的正确初始化值
        if (obj.ready) {
            // 保证看到finalValue=42和normalValue=1
        }
    }
}

10. JMM在开发中的实际应用

10.1 性能优化建议

  • 减少同步范围:只在必要时使用同步
  • 使用volatile代替锁:当只需要可见性保证时
  • 使用线程局部变量:避免共享,消除同步
  • 使用并发容器:代替手动同步的容器

10.2 常见陷阱与避免方法

public class CommonConcurrencyMistakes {
    // 陷阱1:认为volatile保证原子性
    private volatile int count = 0;
    public void unsafeIncrement() {
        count++; // 不是原子操作!
    }
    // 解决方案:使用原子类或同步
    private final AtomicInteger safeCount = new AtomicInteger(0);
    private int synchronizedCount = 0;
    private final Object lock = new Object();
    public void safeIncrement() {
        safeCount.incrementAndGet(); // 方式1:原子类
        synchronized(lock) {         // 方式2:同步
            synchronizedCount++;
        }
    }
    // 陷阱2:误用双重检查锁定
    // 解决方案:使用volatile修饰实例变量
    // 陷阱3:依赖线程优先级
    // 解决方案:不要依赖线程优先级进行正确性设计
    // 陷阱4:在构造函数中启动线程
    public class ProblematicConstructor {
        public ProblematicConstructor() {
            new Thread(() -> {
                // 可能访问未完全初始化的对象
            }).start();
        }
    }
}

总结

Java内存模型是Java并发编程的基石,它通过定义一系列规则和happens-before关系,在多线程环境中提供了内存可见性、原子性和有序性的保证。

关键要点

  • 理解happens-before规则:这是理解线程间操作可见性的核心
  • 正确使用volatile:了解其适用场景和限制
  • 掌握安全发布模式:确保对象在线程间安全共享
  • 避免常见陷阱:识别并避免常见的并发编程错误

JMM既是一个规范也是一个工具,正确理解和使用JMM可以帮助我们编写出既正确又高效的多线程程序。在实际开发中,应该优先使用java.util.concurrent包提供的高级并发工具,它们在大多数情况下都能提供更好的性能和更简单的编程模型。

到此这篇关于Java内存模型(JMM)一文透彻理解的文章就介绍到这了,更多相关Java内存模型JMM内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java实现Kafka生产者和消费者的示例

    Java实现Kafka生产者和消费者的示例

    这篇文章主要介绍了Java实现Kafka生产者和消费者的示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-02-02
  • Java使用JSONObject操作json实例解析

    Java使用JSONObject操作json实例解析

    这篇文章主要介绍了Java使用JSONObject操作json,结合实例形式较为详细的分析了Java使用JSONObject解析json数据相关原理、使用技巧与操作注意事项,需要的朋友可以参考下
    2020-04-04
  • servlet实现文件上传与下载功能

    servlet实现文件上传与下载功能

    这篇文章主要为大家详细介绍了servlet实现文件上传与下载功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-03-03
  • Eclipse配置使用web.xml的方法

    Eclipse配置使用web.xml的方法

    这篇文章主要为大家详细介绍了Eclipse配置使用web.xml的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-03-03
  • Java虚拟机内存分配与回收策略问题精细解读

    Java虚拟机内存分配与回收策略问题精细解读

    Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存,本文让我们来详细了解
    2021-11-11
  • 为何HashSet中使用PRESENT而不是null作为value

    为何HashSet中使用PRESENT而不是null作为value

    这篇文章主要介绍了为何HashSet中使用PRESENT而不是null作为value,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-10-10
  • Java中的包(Package)与导入(Import)示例详解

    Java中的包(Package)与导入(Import)示例详解

    这篇文章主要详细介绍了Java中的包(Package)和导入(Import)概念,包括包的定义、作用、JDK中主要的包、导入的目的与用法、特殊情况的导入、静态导入、包的访问权限和命名规范,文章通过丰富的解释和代码示例,帮助读者深入理解这些概念的实际应用,需要的朋友可以参考下
    2024-11-11
  • java JSP开发之Spring中Bean的使用

    java JSP开发之Spring中Bean的使用

    这篇文章主要介绍了java JSP开发之Spring中Bean的使用的相关资料,在Spring中,bean的生命周期就比较复杂,这里就详细介绍下,需要的朋友可以参考下
    2017-08-08
  • Spring Data JPA 建立表的联合主键

    Spring Data JPA 建立表的联合主键

    这篇文章主要介绍了Spring Data JPA 建立表的联合主键。本文详细的介绍了2种方式,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-04-04
  • Jpa 如何使用@EntityListeners 实现实体对象的自动赋值

    Jpa 如何使用@EntityListeners 实现实体对象的自动赋值

    这篇文章主要介绍了Jpa 如何使用@EntityListeners 实现实体对象的自动赋值,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08

最新评论