当前位置: 首页 > news >正文

Java内存模型(JMM)

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

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

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

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

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

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

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

// 典型的内存可见性问题示例
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方法

// 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. 状态标志

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

    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. 独立观察

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 锁的内存语义

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

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

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

4.3 锁与volatile的比较

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

五、final域的内存语义

5.1 final域的重排序规则

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

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

5.2 final的正确使用

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; // 保证读到1int b = object.y; // 可能读到0}
}

5.3 final与线程安全

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

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 错误的双重检查锁定

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

    private volatile static Instance instance;
  2. 静态内部类

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

    public enum Singleton {INSTANCE;public void someMethod() { ... }
    }

七、JMM与并发工具类

7.1 Atomic类的内存语义

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

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变量保证内存可见性:

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的内存语义

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)

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

    public class FalseSharing {@jdk.internal.vm.annotation.Contendedpublic volatile long value1;@jdk.internal.vm.annotation.Contendedpublic volatile long value2;
    }

8.2 安全发布模式

  1. 静态初始化

    public static Holder holder = new Holder(42);

  2. volatile或AtomicReference

    public volatile Holder holder;

  3. final域

    public class HolderWrapper {public final Holder holder;public HolderWrapper(Holder holder) {this.holder = holder;}
    }

九、JMM与Java新特性

9.1 Java 9+的VarHandle

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

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. 性能考虑

      • 测量而不是猜测

      • 注意伪共享问题

      • 考虑无锁算法

http://www.lryc.cn/news/605262.html

相关文章:

  • 地图可视化实践录:显示高德地图和百度地图
  • ica1靶机攻略
  • C#垃圾回收机制:原理与实践
  • 分享一个FPGA寄存器接口自动化工具
  • 时序数据库厂商 TDengine 发布 AI 原生的工业数据管理平台 IDMP,“无问智推”改变数据消费范式
  • 做题笔记:某大讯飞真题28道
  • 万字深度详解DHCP服务:动态IP地址分配的自动化引擎
  • 100万QPS短链系统如何设计?
  • 基于C语言实现的KV存储引擎(一)
  • 3 运算符与表达式
  • 【CVPR2025】FlowRAM:用区域感知与流匹配加速高精度机器人操作策略学习
  • 架构实战——架构重构内功心法第一式(有的放矢)
  • 《Computational principles and challenges in single-cell data integration》
  • SpringMVC 6+源码分析(一)初始化流程
  • 2021 年 NOI 最后一题题解
  • 项目文档太多、太混乱怎么解决
  • C语言高级(构造数据类型)
  • 2020 年 NOI 最后一题题解
  • REST、GraphQL、gRPC、tRPC深度对比
  • 订阅区块,部署合约,加载合约
  • 颐顿机电携手观远BI数据:以数据驱动决策,领跑先进制造智能化升级
  • 流程制造的数字孪生:从黑箱生产到全息掌控
  • Linux c网络专栏第四章io_uring
  • Linux零基础Shell教学全集(可用于日常查询语句,目录清晰,内容详细)(自学尚硅谷B站shell课程后的万字学习笔记,附课程链接)
  • Baumer工业相机堡盟工业相机如何通过YoloV8的深度学习模型实现汽车牌照的位置识别(C#代码,UI界面版)
  • 大厂主力双塔模型实践与线上服务
  • SSRF漏洞基础
  • 爬虫验证码处理:ddddocr 的详细使用(通用验证码识别OCR pypi版)
  • Redis 中 key 的过期策略 和 定时器的两种实现方式
  • cocos打包web端需要注意的地方