
JUC编程
15、JMM
Java虚拟机提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排
JMM:Java内存模型
关于JMM的一些同步的约定:
- 线程解锁前,必须把共享变量立刻刷回
- 线程加锁前,必须读取主存中的最新值到工作内存中
- 必须保证加锁和解锁是同一把锁
一、JMM是什么
JMM = Java Memory Model(Java内存模型)
它不是一块真实存在的内存,也不是 JVM 的内存结构图,而是一套"并发读写内存时必须遵守的规则"。
一句话理解:JMM规定了,多个线程如何"看见"内存/如何读写共享变量、以及在什么条件下一个线程的修改对另一个线程可见。
二、为什么需要JMM
因为多线程 + CPU 缓存 + 指令重排序会带来三大问题:
- 可见性问题:一个线程改了变量,另一个线程看不到。
- 原子性问题:一个操作被拆成多步,中途被别的线程打断
- 有序性问题 :为了性能,编译器 / CPU 会 重排序指令
JMM就是为了:在不牺牲太多性能的前提下,给 Java 程序员一个"可预期"的并发语义。
三、JMM 的核心抽象:主内存 & 工作内存
JMM 把内存抽象成两层(模型)

- 主内存:所有线程共享
- 工作内存:每个线程私有(可理解为 CPU 缓存 + 寄存器)
线程不能直接操作主内存
线程只能先把变量拷贝到工作内存,再操作
四、JMM的八种内存操作(重点)
JMM 用 8种操作 来描述线程与主内存之间的交互。
| 操作 | 作用对象 | 含义 |
|---|---|---|
| lock | 主内存 | 把主内存中的变量加锁 |
| unlock | 主内存 | 释放锁 |
| read | 主内存→ | 从主内存读取变量的值 |
| load | →工作内存 | 把read的值放入线程工作内存 |
| use | 工作内存→CPU | 线程真正使用变量(参与计算) |
| assign | CPU→工作内存 | 给工作内存中的变量赋新值 |
| store | 工作内存→ | 把工作内存中的值准备写回主内存 |
| write | →主内存 | 把store的值写入主内存 |
五、JMM的核心规定(常考)
- 不允许操作乱序
read后必须loadstore后必须write- 不能单独
read或write
- 工作内存不能凭空产生变量
- 变量必须来自主内存
- 不允许"无中生有"的 assign
- assign 后必须同步回主内存
- 线程不能直接操作主内存
- 所有变量操作必须通过工作内存
- 线程之间 不能互相访问对方的工作内存
- lock / unlock 的约束
- 一个变量同一时刻 只能被一个线程 lock
- lock 会清空工作内存中该变量的旧值
- unlock 前,必须先把修改同步回主内存
- unlock 不能解锁没 lock 的变量
- volatile 的特殊规则
- volatile 写:立即写回主内存
- volatile 读:直接从主内存读
- 禁止指令重排序(内存屏障)
16、volatile
一、volatile是什么
volatile是Java提供的一个 轻量级并发关键字 ,用于修饰 共享变量。
java
volatile boolean flag = true;
volatile三大特性
- 保证共享变量在多线程之间的可见性;
- 禁止指令重排 (通过内存屏障保证有序性);
- 不保证原子性。
二、如何保证可见性
-
什么是可见性:
一个线程对共享变量的修改,能否被其他线程立即看到
-
可见性问题是怎么产生的?
原因不是Java,而是 CPU缓存机制:
- 每个线程都有工作内存(CPU缓存)
- 线程读写的往往是"缓存副本",不是主内存
javapublic class VolatileTest { // 加 volatile可以保证可见性 // private static volatile int num = 0; private static boolean flag = true; public static void main(String[] args) { new Thread(()->{ int i = 0; while(flag) { } }, "A").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } flag = false; System.out.println(flag); } }如果
flag不是 volatile:- 线程 A 可能一直使用缓存中的
true - 永远跳不出循环
-
volatile如何保证可见性?
JMM 明确规定:
- 写 volatile 变量 → 立即刷新到主内存
- 读 volatile 变量 → 强制从主内存中读取
不允许线程使用"旧缓存"
理论依据(happens-before)
在JMM中,A happens-before B,并不是"时间先后",而是:
- A 的结果对 B 可见
- A的执行顺序不能被重排到B之后
JMM规定:对一个 volatile 变量的写 happens-before 任意后续对该 volatile 变量的读
三、不保证原子性
什么是原子性:一个操作要么一次完成,要么完全不发生,中途不能被打断
java
volatile boolean count = 0;
count++;
- 加了 volatile 并不保证线程安全!
为什么 volatile不保证原子性?
count++实际上是三步操作:
- 读取 count
- +1
- 写回 count
即使count是 volatile:线程A、B仍可能同时读到同一个旧值,更新结果被覆盖。
volatile 只能保证:每一步读写是可见的,但三步不能合成一个原子操作
结论
volatile 只能保证"单次读 / 单次写"的原子性,不能保证"复合操作"的原子性
需要使用synchronized或者AtomicInteger解决原子性问题
四、禁止指令重排
什么是指令重排:为了提升性能,编译器和 CPU 允许在不影响单线程结果的前提下调整指令顺序
这是正常且必要的优化。
指令重排在多线程中的问题
java
a = 1;
flag = true;
可能会被重排为:
java
flag = true;
a = 1;
另一线程看到:
java
if(flag) {
// a还是0
}
这就是有序性问题
volatile如何禁止指令重排
volatile写:
- volatile写之前的普通写
- 不能被重排到volatile写之后
volatile读:
- volatile读之后的普通读
- 不能被重排到volatile读之前
实现原理:内存屏障
volatile在读写处插入内存屏障(Memory Barrier):
- 限制指令执行顺序
- 保证前后的操作"看起来有序"
⚠️注意:volatile 不是禁止所有重排,而是禁止"跨 volatile 变量"的重排
17 单例模式
一、什么是单例模式
单例模式保证一个类在整个程序运行期间只有一个实例,并提供一个全局访问点。
核心两点:
- 只能创建一个对象
- 外部可以随时拿到这个对象
二、为什么要用单例模式
常见使用场景
- 配置类(Config)
- 线程池(ThreadPool)
- 日志系统(Logger)
- 缓存管理器
- 连接池管理器
目的总结
| 目的 | 说明 |
|---|---|
| 节省资源 | 避免重复创建重量级对象 |
| 统一状态 | 全局共享同一份数据 |
| 控制访问 | 防止对象被随意 new |
三、单例模式的基本结构
java
class SingleTon {
// 1. 私有构造方法
private SingleTon() {}
// 2. 私有静态实例
private static SingleTon instance;
// 3. 公共静态获取方法
public static SingleTon getInstance() {
if(instance == null) {
instance = new SingleTon();
}
return instance;
}
}
四、单例模式的几种常见实现方法(重点)
单例模式的设计目标
单例模式要同时解决三个问题:
- 唯一性:整个JVM只存在一个实例
- 可访问性:提供全局访问点
- 并发安全性:多线程下不产生多个实例
不同实现方式,本质上是在这三点之间取不同的平衡。
1、饿汉式(Eager Initialization)
-
实现方式
javaclass SingleTon02 { private SingleTon02() {} private final static SingleTon02 INSTANCE = new SingleTon02(); public static SingleTon02 getInstance() { return INSTANCE; } } -
实现原理(非常重要)
- 实例在类加载阶段就创建
- Java的类加载过程由JVM保证线程安全
- 同一个类在JVM中只会被加载一次
因此:类加载 = 天然的同步屏障
-
特点总结
方面 表现 线程安全 ✅(JVM 保证) 懒加载 ❌ 实现复杂度 ⭐ 性能 ⭐⭐⭐⭐ -
优缺点
优点:
- 实现最简单
- 没有并发问题
缺点:
- 不管用不用,类一加载就创建对象
- 如果实例很"重",会浪费资源
-
适用场景
- 实例创建成本低
- 启动即需要使用
- 对资源不敏感的工具类
2、饿汉式(Lazy Initialization) - 线程不安全
-
实现方法
javaclass SingleTon03 { private SingleTon03() {} private static SingleTon03 instance; public static SingleTon03 getInstance() { if(instance == null) { instance = new SingleTon03(); } return instance; } } -
实现原理
- 第一次调用时才创建对象
- 依赖普通的if判断
线程不安全!
多线程下可能发生:
markdown线程 A:if (instance == null) → true 线程 B:if (instance == null) → true 线程 A:new Singleton() 线程 B:new Singleton()违背"唯一性"
-
特点总结
方面 表现 线程安全 ❌ 懒加载 ✅ 推荐程度 ❌ -
结论
只存在于"教材"和"反例"中,实际开发中不使用。
3、同步方法懒汉式 - 线程安全但性能较差
-
实现方式
javaclass SingleTon04 { private SingleTon04() {} private static SingleTon04 instance; public static synchronized SingleTon04 getInstance() { if(instance == null) { instance = new SingleTon04(); } return instance; } } -
实现原理
- 利用
synchronized:同一时间只有一个线程进入方法 - 保证
instance创建过程互斥
- 利用
-
特点总结
方面 表现 线程安全 ✅ 懒加载 ✅ 性能 ❌(每次调用都加锁) -
结论
实际上:
- 只有第一次创建需要同步
- 但后续每次访问都要加锁
锁的粒度过大,性价比低
能用,但几乎没有使用价值
4、双重检查锁(DCL + volatile)- 重点
-
实现方式
javaclass SingleTon05 { private SingleTon05() {} private static volatile SingleTon05 instance; public static SingleTon05 getInstance() { if(instance == null) { synchronized (SingleTon05.class) { if(instance == null) { instance = new SingleTon05(); } } } return instance; } } -
实现原理
为什么要"双重检查"?(重要)
- 外层
if:避免不必要的同步(提高性能) - 内层
if:防止多个线程同时创建实例
为什么必须加
volatile?如果 DCL 中没有使用
volatile,对象可能在"尚未完全初始化"时就被其他线程看到并使用,从而导致程序在运行期出现不可预测的错误,包括空指针、状态错乱、违反不变量等严重问题。对象创建不是原子操作,可能被重排:
markdown正常顺序: 1.分配内存 2.初始化对象 3.instance指向内存 可能被重排为: 1 → 3 → 2结果:
- 其他线程看到
instance != null - 但对象还没初始化完成
volatile的作用:- 禁止指令重排
- 保证可见性
- 外层
-
特点总结
方面 表现 线程安全 ✅ 懒加载 ✅ 性能 ⭐⭐⭐⭐ 理解难度 ⭐⭐⭐⭐ -
总结
工程可用,但代码复杂、容易写错
5、静态内部类(最推荐方案)
-
实现方法
javaclass SingleTon06 { private SingleTon06() {} private static class Holder { private final static SingleTon06 INSTANCE = new SingleTon06(); } public static SingleTon06 getInstance() { return Holder.INSTANCE; } } -
实现原理
- 外部类加载时:不会加载静态内部类
- 第一次调用
getInstance():- 触发
Holder类加载 - JVM在类加载阶段创建INSTANCE
- 触发
- JVM保证类加载线程安全
-
特点总结
方面 表现 线程安全 ✅(类加载) 懒加载 ✅ 性能 ⭐⭐⭐⭐⭐ 代码复杂度 ⭐⭐ -
总结
综合最优解,实际项目中最推荐
6、枚举单例(最安全)
-
实现方式
javaenum SingleTon07 { INSTANCE; } -
实现原理
- 枚举本质:由JVM在类加载阶段创建
- JVM:
- 保证线程安全
- 防止反射创建
- 防止反序列化破坏
-
特点总结
方面 表现 线程安全 ✅ 反射攻击 ❌ 反序列化 ❌ 灵活性 ❌ -
总结
理论上最安全,但工程灵活性一般
7、实现方法对比
| 实现方式 | 线程安全 | 懒加载 | 推荐度 |
|---|---|---|---|
| 饿汉式 | ✅ | ❌ | ⭐⭐⭐ |
| 懒汉式 | ❌ | ✅ | ❌ |
| 同步方法 | ✅ | ✅ | ⭐⭐ |
| DCL + volatile | ✅ | ✅ | ⭐⭐⭐⭐ |
| 静态内部类 | ✅ | ✅ | ⭐⭐⭐⭐⭐ |
| 枚举 | ✅ | ❌ | ⭐⭐⭐⭐ |
单例模式的核心在于"唯一实例 + 线程安全"。在所有实现方式中,静态内部类是工程上最推荐的方案,而 DCL + volatile 是并发原理考察的重点。