JMM与内存可见性
一、Java 内存模型(JMM)概述
1.1 什么是 JMM?
定义:Java Memory Model(JMM)是 Java 虚拟机规范定义的一种内存模型,用于屏蔽各种硬件和操作系统的内存访问差异,实现跨平台的内存访问一致性。
核心目标:
- 定义程序中变量的访问规则
- 保证多线程环境下的内存可见性
- 规定指令重排序的边界
⚠️ 注意区分:
- JVM 内存结构:堆、栈、方法区等物理内存布局
- JMM:线程间通信的抽象模型(规范)
1.2 JMM 抽象模型
┌─────────────────────────────────────────────────┐
│ 主内存(Main Memory) │
│ 所有线程共享的变量存储区域 │
│ [对象实例] [静态变量] [常量] │
└────────┬──────────────────────────┬──────────────┘
│ │
读取 │ 写入 读取 │ 写入
↓ ↓
┌────────────────────┐ ┌────────────────────┐
│ 线程 A 工作内存 │ │ 线程 B 工作内存 │
│ [变量副本] │ │ [变量副本] │
│ [CPU 缓存] │ │ [CPU 缓存] │
└────────────────────┘ └────────────────────┘
关键概念:
| 术语 | 说明 | 对应硬件 |
|---|---|---|
| 主内存 | 所有线程共享的内存区域 | RAM(主存) |
| 工作内存 | 每个线程私有的内存区域 | CPU 缓存(L1/L2/L3)+ 寄存器 |
| 变量副本 | 线程从主内存读取变量到工作内存 | 缓存行(Cache Line) |
1.3 JMM 定义的 8 种操作
JMM 规定了主内存与工作内存之间的交互协议:
| 操作 | 作用位置 | 说明 |
|---|---|---|
| lock | 主内存 | 锁定变量(独占访问) |
| unlock | 主内存 | 解锁变量 |
| read | 主内存 | 读取变量到工作内存 |
| load | 工作内存 | 将 read 的值放入工作内存副本 |
| use | 工作内存 | 将工作内存变量传递给执行引擎 |
| assign | 工作内存 | 将执行引擎的值赋给工作内存变量 |
| store | 工作内存 | 将工作内存变量传输到主内存 |
| write | 主内存 | 将 store 的值写入主内存 |
示例流程:
// 线程 A 执行:x = 1;
1. assign: 工作内存 x_副本 = 1
2. store: 将 x_副本 传输到主内存
3. write: 主内存 x = 1
// 线程 B 执行:int y = x;
1. read: 从主内存读取 x
2. load: 加载到工作内存 x_副本
3. use: 将 x_副本 赋值给 y
二、内存可见性问题
2.1 经典案例:循环不停止
public class VisibilityProblem {
private static boolean flag = false; // ⚠️ 问题变量
public static void main(String[] args) throws InterruptedException {
// 线程 A:读取 flag
Thread threadA = new Thread(() -> {
while (!flag) {
// 死循环
}
System.out.println("线程 A 结束");
});
// 线程 B:修改 flag
Thread threadB = new Thread(() -> {
flag = true;
System.out.println("线程 B 已将 flag 设置为 true");
});
threadA.start();
Thread.sleep(100);
threadB.start();
}
}
// 运行结果:
// 线程 B 已将 flag 设置为 true
// (线程 A 永远不会结束,死循环)
问题分析:
时间线:
t1: 线程 A 从主内存读取 flag = false 到工作内存
t2: 线程 A 进入循环,一直使用工作内存的 flag(false)
t3: 线程 B 修改主内存 flag = true
t4: 线程 A 的工作内存未感知到变化,继续使用旧值 false
根本原因:CPU 缓存导致的可见性问题
硬件层面解释:
CPU 架构:
┌─────────┐ ┌─────────┐
│ Core 0 │ │ Core 1 │
│ L1 Cache│ │ L1 Cache│
│ flag=0 │ │ flag=1 │ ← 各自缓存不一致
└────┬────┘ └────┬────┘
└───────┬───────┘
L3 Cache
│
主内存 RAM
2.2 解决方案对比
| 方案 | 代码示例 | 性能 | 适用场景 |
|---|---|---|---|
| 1. volatile | volatile boolean flag |
高 | 状态标志、单次赋值 |
| 2. synchronized | synchronized(lock) { flag = true; } |
低 | 需要原子性保证 |
| 3. AtomicBoolean | AtomicBoolean flag = new AtomicBoolean() |
中 | CAS 操作 |
| 4. final | final boolean flag = true |
极高 | 不可变对象 |
最佳实践(使用 volatile):
public class VisibilityFixed {
private static volatile boolean flag = false; // ✅ 添加 volatile
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
while (!flag) {
// 可以正常退出
}
System.out.println("线程 A 结束");
});
Thread threadB = new Thread(() -> {
flag = true;
System.out.println("线程 B 已将 flag 设置为 true");
});
threadA.start();
Thread.sleep(100);
threadB.start();
}
}
// 运行结果:
// 线程 B 已将 flag 设置为 true
// 线程 A 结束 ✅
2.3 可见性问题的三种表现
表现 1:读取不到最新值(上述案例)
表现 2:读取到部分更新的值
public class PartialVisibility {
private static long value = 0L; // 64 位(非原子操作)
public static void main(String[] args) {
// 线程 A:写入 Long.MAX_VALUE
new Thread(() -> {
value = Long.MAX_VALUE; // 0x7FFFFFFFFFFFFFFF
}).start();
// 线程 B:读取 value
new Thread(() -> {
long v = value;
System.out.println(v);
// 可能输出:4294967295(只读到低 32 位)⚠️
}).start();
}
}
// 原因:
// 在 32 位系统上,long 的读写分为两步:
// 1. 写入低 32 位
// 2. 写入高 32 位
// 线程 B 可能在步骤 1 和 2 之间读取
表现 3:指令重排序导致的问题(见第三节)
三、重排序
3.1 什么是重排序?
定义:编译器和处理器为了优化程序性能,对指令执行顺序进行调整。
三种重排序:
源代码
↓
编译器优化重排序(javac / JIT)
↓
指令级并行重排序(CPU)
↓
内存系统重排序(缓存、写缓冲)
↓
最终执行顺序
3.2 重排序示例
示例 1:编译器重排序
// 原始代码
int a = 1;
int b = 2;
int c = a + b;
// 可能重排为(c 不依赖 b):
int a = 1;
int c = a + ...; // 先计算部分
int b = 2;
int c = a + b;
示例 2:CPU 指令重排序
// 原始代码
a = 1; // 写操作 1
b = 2; // 写操作 2
// CPU 可能重排为(写缓冲优化):
b = 2; // 先写 b(缓冲区有空闲)
a = 1; // 后写 a
3.3 重排序的经典问题:双重检查锁(DCL)
public class Singleton {
private static Singleton instance; // ⚠️ 问题:未加 volatile
public static Singleton getInstance() {
if (instance == null) { // 检查 1
synchronized (Singleton.class) {
if (instance == null) { // 检查 2
instance = new Singleton(); // ⚠️ 关键代码
}
}
}
return instance;
}
}
问题分析:
// instance = new Singleton() 实际分为 3 步:
1. memory = allocate(); // 分配内存
2. ctorInstance(memory); // 初始化对象
3. instance = memory; // 设置引用
// ⚠️ 可能重排序为:
1. memory = allocate();
3. instance = memory; // 先设置引用(此时对象未初始化)
2. ctorInstance(memory); // 后初始化
// 线程 A 执行到步骤 3
// 线程 B 执行检查 1,发现 instance != null
// 线程 B 返回 instance(未初始化的对象)→ 使用时 NPE
执行时序图:
线程 A 线程 B
──────────────────────────────────────────
分配内存
instance = memory ────────→ if (instance == null) ✗
return instance; ⚠️ 半初始化对象
初始化对象(太晚了)
正确写法:
public class Singleton {
private static volatile Singleton instance; // ✅ 添加 volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // volatile 禁止重排序
}
}
}
return instance;
}
}
3.4 as-if-serial 语义
定义:不管怎么重排序,单线程程序的执行结果不能改变。
// 示例:
int a = 1;
int b = 2;
int c = a + b;
// 重排序规则:
// ✅ 允许:a = 1; int b = 2; 交换顺序(无依赖)
// ❌ 禁止:c = a + b; 移到 a = 1 之前(有依赖)
// 保证单线程结果:c = 3
数据依赖性:
写后读(RAW):a = 1; b = a; // b 依赖 a
写后写(WAW):a = 1; a = 2; // 第二个写覆盖第一个
读后写(WAR):b = a; a = 1; // a 的新值不能影响 b 的旧读取
⚠️ 有数据依赖的操作不会重排序(单线程内)
四、happens-before 原则
4.1 定义
happens-before:JMM 定义的操作偏序关系,如果 A happens-before B,则 A 的结果对 B 可见。
核心作用:
- 程序员:不需要理解底层重排序,只需遵循 happens-before 规则
- JVM:在不违反 happens-before 的前提下,可以自由优化
4.2 八大 happens-before 规则
规则 1:程序顺序规则(Program Order Rule)
定义:单线程内,按照代码顺序执行。
int a = 1; // 操作 A
int b = 2; // 操作 B
int c = a + b; // 操作 C
// happens-before 关系:
// A happens-before B
// B happens-before C
// 结论:C 读取的 a 和 b 一定是最新值
⚠️ 注意:允许重排序,但必须保证结果一致(as-if-serial)
int a = 1;
int b = 2; // 可能与 a 重排序(无依赖)
int c = a + b; // 但 c 一定在 a, b 之后
规则 2:volatile 变量规则(Volatile Variable Rule)
定义:对 volatile 变量的写操作 happens-before 后续对该变量的读操作。
public class VolatileExample {
private volatile boolean flag = false;
private int data = 0;
// 线程 A
public void writer() {
data = 42; // ① 普通写
flag = true; // ② volatile 写
}
// 线程 B
public void reader() {
if (flag) { // ③ volatile 读
int x = data; // ④ 普通读
// x 一定等于 42 ✅
}
}
}
// happens-before 链:
// ① happens-before ②(程序顺序规则)
// ② happens-before ③(volatile 规则)
// ③ happens-before ④(程序顺序规则)
// 结论:① happens-before ④(传递性)
内存语义图:
线程 A 线程 B
──────────────────────────────────────────
data = 42
flag = true (volatile 写)
│
│ volatile 内存屏障
└────────────────────→ if (flag) volatile 读
int x = data; ✅ 看到 42
规则 3:锁规则(Monitor Lock Rule)
定义:对锁的解锁 happens-before 后续对该锁的加锁。
public class SynchronizedExample {
private int data = 0;
// 线程 A
public synchronized void writer() {
data = 42; // ① 写操作
} // ② 解锁
// 线程 B
public synchronized void reader() { // ③ 加锁
int x = data; // ④ 读操作
// x 一定等于 42 ✅
}
}
// happens-before 链:
// ① happens-before ②(程序顺序规则)
// ② happens-before ③(锁规则)
// ③ happens-before ④(程序顺序规则)
// 结论:① happens-before ④
图示:
线程 A Monitor 线程 B
─────────────────────────────────────────────────
data = 42
unlock ────────→ [锁释放] ────────→ lock
int x = data; ✅
规则 4:线程启动规则(Thread Start Rule)
定义:Thread.start() happens-before 该线程的任何操作。
public class ThreadStartExample {
private int data = 0;
public static void main(String[] args) {
ThreadStartExample obj = new ThreadStartExample();
obj.data = 42; // ① 主线程写
Thread thread = new Thread(() -> {
int x = obj.data; // ③ 子线程读
// x 一定等于 42 ✅
});
thread.start(); // ② start() 操作
}
}
// happens-before 链:
// ① happens-before ②(程序顺序规则)
// ② happens-before ③(线程启动规则)
// 结论:① happens-before ③
规则 5:线程终止规则(Thread Termination Rule)
定义:线程的所有操作 happens-before 其他线程检测到该线程终止(join() 返回)。
public class ThreadJoinExample {
private int data = 0;
public static void main(String[] args) throws InterruptedException {
ThreadJoinExample obj = new ThreadJoinExample();
Thread thread = new Thread(() -> {
obj.data = 42; // ① 子线程写
});
thread.start();
thread.join(); // ② 等待子线程结束
int x = obj.data; // ③ 主线程读
// x 一定等于 42 ✅
}
}
// happens-before 链:
// ① happens-before ②(线程终止规则)
// ② happens-before ③(程序顺序规则)
// 结论:① happens-before ③
规则 6:线程中断规则(Thread Interruption Rule)
定义:对线程 interrupt() 的调用 happens-before 被中断线程检测到中断事件。
public class InterruptExample {
private int data = 0;
public static void main(String[] args) throws InterruptedException {
InterruptExample obj = new InterruptExample();
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 工作中...
}
int x = obj.data; // ③ 读取
// x 一定能看到主线程的修改 ✅
});
thread.start();
obj.data = 42; // ① 主线程写
thread.interrupt(); // ② 中断
}
}
// happens-before 链:
// ① happens-before ②(程序顺序规则)
// ② happens-before ③(线程中断规则)
// 结论:① happens-before ③
规则 7:对象终结规则(Finalizer Rule)
定义:对象的构造函数结束 happens-before finalize() 方法的开始。
public class FinalizerExample {
private int data;
public FinalizerExample() {
data = 42; // ① 构造函数
} // ② 构造结束
@Override
protected void finalize() { // ③ finalize 开始
int x = data; // ④ 读取
// x 一定等于 42 ✅
}
}
// happens-before 链:
// ① happens-before ②(程序顺序规则)
// ② happens-before ③(对象终结规则)
// ③ happens-before ④(程序顺序规则)
⚠️ 注意:finalize() 已被废弃(JDK 9+),不推荐使用。
规则 8:传递性规则(Transitivity)
定义:如果 A happens-before B,B happens-before C,则 A happens-before C。
public class TransitivityExample {
private volatile boolean ready = false;
private int data = 0;
// 线程 A
public void init() {
data = 42; // ① 写 data
ready = true; // ② volatile 写
}
// 线程 B
public void use() {
if (ready) { // ③ volatile 读
int x = data; // ④ 读 data
// x 一定等于 42 ✅
}
}
}
// happens-before 链:
// ① happens-before ②(程序顺序规则)
// ② happens-before ③(volatile 规则)
// ③ happens-before ④(程序顺序规则)
// 通过传递性:① happens-before ④ ✅
4.3 happens-before 与时间顺序的关系
⚠️ 重要:happens-before 不等于时间先后顺序!
int a = 1; // ①
int b = 2; // ②
// ① happens-before ②(程序顺序规则)
// 但实际执行可能:② 先执行,① 后执行(重排序)
// 只要保证:如果 ② 依赖 ①,则 ① 的结果对 ② 可见
示例:允许重排序的情况
public class ReorderExample {
private int a = 0;
private int b = 0;
public void method() {
a = 1; // ①
b = 2; // ②
int c = a + b; // ③
}
}
// happens-before 关系:
// ① happens-before ③
// ② happens-before ③
// 但 ① 和 ② 之间无 happens-before 关系 → 可以重排序
// 可能的执行顺序:
// b = 2; a = 1; int c = a + b; ✅ 合法
五、volatile 关键字
5.1 volatile 的两大特性
| 特性 | 说明 | 示例 |
|---|---|---|
| 可见性 | 写操作立即刷新到主内存,读操作从主内存读取 | 状态标志 |
| 有序性 | 禁止指令重排序(内存屏障) | 双重检查锁 |
⚠️ 不保证原子性:
private volatile int count = 0;
count++; // ❌ 非原子操作(读-改-写)
// 等价于:
// int temp = count; // 读
// temp = temp + 1; // 改
// count = temp; // 写
// 多线程下会丢失更新
5.2 volatile 的内存语义
写操作的内存语义
volatile boolean flag = false;
// 写操作:flag = true
1. 将当前线程工作内存中的所有变量刷新到主内存
2. 写入 flag = true 到主内存
3. 插入 StoreLoad 屏障(禁止后续读操作重排序到写之前)
读操作的内存语义
// 读操作:if (flag)
1. 插入 LoadLoad 屏障(禁止后续读操作重排序到 flag 读取之前)
2. 从主内存读取 flag 的最新值
3. 清空工作内存,后续读取从主内存加载
5.3 volatile 的内存屏障
四种内存屏障:
| 屏障类型 | 指令示例 | 说明 |
|---|---|---|
| LoadLoad | Load1; LoadLoad; Load2 | Load1 的数据必须先加载 |
| StoreStore | Store1; StoreStore; Store2 | Store1 的数据必须先刷新 |
| LoadStore | Load1; LoadStore; Store2 | Load1 必须先完成 |
| StoreLoad | Store1; StoreLoad; Load2 | Store1 必须全局可见后才能 Load2 |
volatile 写操作插入的屏障:
普通写/volatile 写
StoreStore 屏障 ← 禁止上面的写与 volatile 写重排序
volatile 写
StoreLoad 屏障 ← 禁止 volatile 写与下面的读重排序
普通读/volatile 读
volatile 读操作插入的屏障:
普通读/volatile 读
LoadLoad 屏障 ← 禁止下面的读与 volatile 读重排序
volatile 读
LoadStore 屏障 ← 禁止下面的写与 volatile 读重排序
普通写/volatile 写
5.4 volatile 的适用场景
✅ 场景 1:状态标志
public class StatusFlag {
private volatile boolean shutdown = false;
public void shutdown() {
shutdown = true; // 写线程
}
public void doWork() {
while (!shutdown) { // 读线程
// 执行任务
}
}
}
✅ 场景 2:双重检查锁(DCL)
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 User currentUser;
// 写线程(单线程)
public void updateUser(User user) {
currentUser = user;
}
// 读线程(多线程)
public User getCurrentUser() {
return currentUser;
}
}
❌ 场景 4:计数器(需要原子性)
public class Counter {
private volatile int count = 0;
public void increment() {
count++; // ❌ 非原子操作,会丢失更新
}
// 正确做法:
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // ✅ CAS 保证原子性
}
}
5.5 volatile vs synchronized
| 对比项 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ 保证 | ✅ 保证 |
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 有序性 | ✅ 保证(禁止重排序) | ✅ 保证(锁机制) |
| 性能 | 高(无锁) | 低(加锁开销) |
| 阻塞 | 不阻塞 | 阻塞 |
| 适用场景 | 状态标志、单次赋值 | 复合操作、临界区 |
六、final 关键字的内存语义
6.1 final 的三种使用场景
| 使用位置 | 说明 | 内存语义 |
|---|---|---|
| final 变量 | 基本类型/引用类型 | 一次赋值,不可修改 |
| final 方法 | 方法不可重写 | 无特殊内存语义 |
| final 类 | 类不可继承 | 无特殊内存语义 |
本节重点讨论:final 域(成员变量)的内存语义
6.2 final 域的重排序规则
规则 1:final 域的写入重排序规则
定义:禁止将 final 域的写操作重排序到构造函数之外。
public class FinalExample {
private final int x;
private int y;
private static FinalExample obj;
public FinalExample() {
x = 1; // ① final 域写入
y = 2; // ② 普通域写入
} // ③ 构造函数结束
public static void writer() {
obj = new FinalExample(); // ④ 对象引用赋值
}
public static void reader() {
FinalExample o = obj; // ⑤ 读取引用
int a = o.x; // ⑥ 读取 final 域
int b = o.y; // ⑦ 读取普通域
}
}
重排序分析:
// ✅ 允许的重排序(普通域):
obj = new FinalExample(); // 先赋值引用
y = 2; // 后初始化 y(⚠️ 可能导致读到 0)
// ❌ 禁止的重排序(final 域):
obj = new FinalExample(); // 先赋值引用
x = 1; // 后初始化 x(禁止!)
// 保证:如果读线程看到 obj != null,则 x 一定初始化完成
内存屏障实现(JVM 层面):
构造函数:
x = 1; // final 域写入
y = 2; // 普通域写入
StoreStore 屏障 ← 插入内存屏障
} // 构造函数结束
obj = new FinalExample(); // 对象引用赋值
规则 2:final 域的读取重排序规则
定义:禁止初次读取对象引用与读取 final 域重排序。
public static void reader() {
FinalExample o = obj; // ① 读取引用
int a = o.x; // ② 读取 final 域
}
// ❌ 禁止重排序为:
int a = o.x; // 先读 x(此时 o 可能为 null)
FinalExample o = obj; // 后读引用
内存屏障实现:
FinalExample o = obj; // 读取引用
LoadLoad 屏障 ← 插入内存屏障
int a = o.x; // 读取 final 域
6.3 final 引用类型的特殊规则
场景:final 域引用的对象
public class FinalReferenceExample {
private final int[] arr; // final 引用数组
public FinalReferenceExample() {
arr = new int[10]; // ① 分配数组
arr[0] = 1; // ② 初始化数组元素
}
private static FinalReferenceExample obj;
public static void writer() {
obj = new FinalReferenceExample(); // ③ 赋值引用
}
public static void reader() {
FinalReferenceExample o = obj; // ④ 读取引用
int x = o.arr[0]; // ⑤ 读取数组元素
// x 一定等于 1 ✅
}
}
额外规则:
构造函数内对 final 引用对象的成员域的写入:
arr = new int[10];
arr[0] = 1; ← 这个写入也受 final 保护
StoreStore 屏障
} // 构造函数结束
// 保证:读线程看到 obj != null 时,arr[0] 一定初始化完成
⚠️ 注意:final 只保护构造函数内的初始化,不保护后续修改!
public class FinalMutableExample {
private final List<String> list = new ArrayList<>();
public FinalMutableExample() {
list.add("A"); // ✅ 构造函数内,受保护
}
public void addElement(String s) {
list.add(s); // ❌ 构造函数外,不受 final 保护
// 需要额外的同步机制
}
}
6.4 final 的实际应用场景
应用 1:安全发布不可变对象
// 不使用 final(不安全)
public class UnsafePoint {
private int x;
private int y;
public UnsafePoint(int x, int y) {
this.x = x;
this.y = y;
}
}
// 问题:
Point p = new Point(1, 2);
// 另一个线程读取 p.x 可能读到 0(重排序导致)
// 使用 final(安全)
public class SafePoint {
private final int x; // ✅ final 保证可见性
private final int y;
public SafePoint(int x, int y) {
this.x = x;
this.y = y;
}
}
// 保证:任何线程读取 p.x 一定读到 1
应用 2:String 类的不可变性
public final class String {
private final char[] value; // ✅ final 保证安全发布
public String(String original) {
this.value = original.value;
// final 保证其他线程读取 value 一定是初始化后的
}
}
// 优势:
// 1. 无需同步即可在多线程间共享
// 2. 可以安全地作为 HashMap 的 key
// 3. 字符串常量池优化
应用 3:享元模式(Flyweight)
public final class Integer {
private final int value; // ✅ final 不可变
// 缓存 -128 到 127
private static class IntegerCache {
static final Integer[] cache = new Integer[256];
static {
for (int i = 0; i < cache.length; i++) {
cache[i] = new Integer(i - 128);
}
}
}
public static Integer valueOf(int i) {
if (i >= -128 && i <= 127) {
return IntegerCache.cache[i + 128]; // 安全共享
}
return new Integer(i);
}
}
6.5 final 与 volatile 的对比
| 对比项 | final | volatile |
|---|---|---|
| 可见性 | ✅ 构造函数完成后可见 | ✅ 每次读写都可见 |
| 原子性 | N/A(不涉及修改) | ❌ 不保证 |
| 有序性 | ✅ 禁止构造函数重排序 | ✅ 禁止读写重排序 |
| 修改 | ❌ 不可修改 | ✅ 可以修改 |
| 性能 | 最高(编译期优化) | 高(无锁) |
| 使用场景 | 不可变对象、常量 | 状态标志、单次赋值 |
七、实战案例
案例 1:单例模式的演进
版本 1:懒加载(线程不安全)
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // ❌ 多线程下会创建多个实例
instance = new Singleton();
}
return instance;
}
}
问题:
线程 A 线程 B
──────────────────────────────────────────
if (instance == null) ✓
if (instance == null) ✓
instance = new Singleton();
instance = new Singleton();
// 创建了两个实例 ❌
版本 2:加锁(性能差)
public class Singleton {
private static Singleton instance;
public static synchronized Singleton getInstance() { // ✅ 线程安全
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
问题:每次调用都要加锁,性能差(即使已经初始化)。
版本 3:双重检查锁(DCL,有问题)
public class Singleton {
private static Singleton instance; // ⚠️ 缺少 volatile
public static Singleton getInstance() {
if (instance == null) { // 检查 1(无锁)
synchronized (Singleton.class) {
if (instance == null) { // 检查 2(加锁)
instance = new Singleton(); // ⚠️ 问题代码
}
}
}
return instance;
}
}
问题分析(重排序导致):
// instance = new Singleton() 分解为:
1. memory = allocate(); // 分配内存
2. ctorInstance(memory); // 初始化对象
3. instance = memory; // 设置引用
// ⚠️ 可能重排序为:
1. memory = allocate();
3. instance = memory; // 先设置引用(对象未初始化)
2. ctorInstance(memory); // 后初始化
// 时序图:
线程 A 线程 B
──────────────────────────────────────────
分配内存
instance = memory ────────→ if (instance == null) ✗
return instance; ⚠️ 半初始化对象
初始化对象(太晚了)
版本 4:DCL + volatile(正确)✅
public class Singleton {
private static volatile Singleton instance; // ✅ 添加 volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // volatile 禁止重排序
}
}
}
return instance;
}
}
volatile 的作用:
// volatile 写操作会插入 StoreStore 屏障:
memory = allocate();
ctorInstance(memory);
StoreStore 屏障 ← 禁止上面的初始化与下面的引用赋值重排序
instance = memory;
// 保证:读线程看到 instance != null 时,对象一定初始化完成
版本 5:静态内部类(推荐)✅
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // 类加载时初始化,天然线程安全
}
}
优势:
- 懒加载:Holder 类在 getInstance() 调用时才加载
- 线程安全:类加载由 JVM 保证同步
- 性能好:无锁
版本 6:枚举(最佳实践)✅
public enum Singleton {
INSTANCE;
public void doSomething() {
// 业务方法
}
}
// 使用:
Singleton.INSTANCE.doSomething();
优势:
- 天然单例(JVM 保证)
- 线程安全
- 防止反序列化破坏单例
- 防止反射攻击
案例 2:生产者-消费者模式(volatile 实现)
public class ProducerConsumer {
private volatile boolean hasData = false;
private int data;
// 生产者
public void produce(int value) {
data = value; // ① 写数据
hasData = true; // ② volatile 写
}
// 消费者
public int consume() {
while (!hasData) { // ③ volatile 读
// 自旋等待
}
int result = data; // ④ 读数据
hasData = false;
return result; // result 一定等于 value ✅
}
}
// happens-before 链:
// ① happens-before ②(程序顺序规则)
// ② happens-before ③(volatile 规则)
// ③ happens-before ④(程序顺序规则)
// 结论:① happens-before ④(传递性)
案例 3:配置类的安全发布
// 不安全的发布
public class UnsafeConfig {
private Map<String, String> config;
public UnsafeConfig() {
config = new HashMap<>();
config.put("url", "http://example.com");
config.put("timeout", "30");
}
// 问题:其他线程可能读到未初始化的 config 或部分初始化的 Map
}
// 安全发布(使用 final)
public class SafeConfig {
private final Map<String, String> config; // ✅ final 保证可见性
public SafeConfig() {
Map<String, String> temp = new HashMap<>();
temp.put("url", "http://example.com");
temp.put("timeout", "30");
config = Collections.unmodifiableMap(temp); // ✅ 不可变
}
public String getConfig(String key) {
return config.get(key); // 线程安全,无需同步
}
}
// 保证:任何线程读取 config 时,Map 已完全初始化
案例 4:延迟初始化占位符(Holder)
public class ResourceFactory {
// 重量级资源
private static class ResourceHolder {
static final Resource resource = new Resource(); // ✅ 类加载时初始化
}
public static Resource getResource() {
return ResourceHolder.resource; // 懒加载 + 线程安全
}
}
// 类加载时序:
// 1. ResourceFactory 类加载
// 2. 首次调用 getResource() 时,触发 ResourceHolder 类加载
// 3. 初始化 static final resource(JVM 保证线程安全)
案例 5:volatile 数组问题(常见误区)
// ❌ 错误示例
public class VolatileArray {
private volatile int[] arr = new int[10];
public void writer() {
arr[0] = 1; // ⚠️ 数组元素的修改不受 volatile 保护
}
public int reader() {
return arr[0]; // ⚠️ 可能读到旧值
}
}
// 问题分析:
// volatile 只保证 arr 引用的可见性
// 不保证 arr[0] 元素的可见性
// ✅ 正确做法 1:使用 AtomicIntegerArray
public class AtomicArrayExample {
private AtomicIntegerArray arr = new AtomicIntegerArray(10);
public void writer() {
arr.set(0, 1); // ✅ 原子操作 + 可见性
}
public int reader() {
return arr.get(0); // ✅ 读取最新值
}
}
// ✅ 正确做法 2:使用 synchronized
public class SyncArrayExample {
private int[] arr = new int[10];
public synchronized void writer() {
arr[0] = 1; // ✅ 锁保证可见性
}
public synchronized int reader() {
return arr[0]; // ✅ 读取最新值
}
}
八、面试高频问题
Q1:什么是 Java 内存模型(JMM)?
参考答案:
JMM 是 Java 虚拟机规范定义的一种内存模型,用于屏蔽各种硬件和操作系统的内存访问差异。
核心概念:
- 主内存:所有线程共享的内存区域
- 工作内存:每个线程私有的内存区域(CPU 缓存)
- 内存交互:定义了 8 种操作(read、load、use、assign、store、write、lock、unlock)
作用:
- 定义变量的访问规则
- 保证多线程环境下的内存可见性
- 规定指令重排序的边界(happens-before)
与 JVM 内存结构的区别:
- JVM 内存结构:堆、栈、方法区等物理内存布局
- JMM:线程间通信的抽象模型(规范)
Q2:什么是内存可见性问题?如何解决?
参考答案:
定义:一个线程修改了共享变量的值,其他线程不能立即看到修改后的值。
原因:
CPU 缓存架构:
线程 A(Core 0) 线程 B(Core 1)
↓ ↓
L1 Cache L1 Cache
flag=false flag=false ← 各自缓存
└────────┬────────────┘
L3 Cache
↓
主内存 RAM
flag=true
解决方案:
| 方案 | 使用场景 | 性能 |
|---|---|---|
| volatile | 状态标志、单次赋值 | 高 |
| synchronized | 需要原子性保证 | 低 |
| Lock | 复杂同步逻辑 | 中 |
| Atomic 类 | CAS 操作 | 中 |
| final | 不可变对象 | 极高 |
示例:
// 问题代码
private static boolean flag = false;
// 解决方案
private static volatile boolean flag = false; // ✅
Q3:volatile 有什么作用?能保证原子性吗?
参考答案:
作用(两大特性):
-
可见性:
- 写操作:立即刷新到主内存
- 读操作:从主内存读取最新值
-
有序性:
- 禁止指令重排序(通过内存屏障)
- 保证 volatile 变量前后的操作不会被重排序
不保证原子性:
private volatile int count = 0;
count++; // ❌ 非原子操作
// 分解为:
// 1. temp = count; (读)
// 2. temp = temp + 1; (改)
// 3. count = temp; (写)
// 多线程下会丢失更新
// 正确做法:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // ✅ CAS 保证原子性
内存屏障实现:
volatile 写:
StoreStore 屏障
volatile 写
StoreLoad 屏障
volatile 读:
LoadLoad 屏障
volatile 读
LoadStore 屏障
Q4:什么是重排序?为什么要重排序?
参考答案:
定义:编译器和处理器为了优化性能,对指令执行顺序进行调整。
三种重排序:
源代码
↓
1. 编译器优化重排序(javac/JIT)
↓
2. 指令级并行重排序(CPU 流水线)
↓
3. 内存系统重排序(缓存、写缓冲)
↓
最终执行顺序
为什么要重排序?
- 性能优化:充分利用 CPU 流水线、缓存
- 减少等待:避免等待慢速内存操作
限制条件:
- as-if-serial:单线程内结果不变
- 数据依赖性:有依赖的操作不会重排序
示例:
int a = 1; // ①
int b = 2; // ②
int c = a + b; // ③
// 允许重排序:① 和 ② 交换(无依赖)
// 禁止重排序:③ 不能移到 ① 或 ② 之前(有依赖)
多线程问题:
// 线程 A
x = 1;
flag = true;
// 线程 B
if (flag) {
int y = x; // 可能读到 x = 0(重排序导致)
}
Q5:什么是 happens-before 原则?
参考答案:
定义:JMM 定义的操作偏序关系,如果 A happens-before B,则 A 的结果对 B 可见。
核心作用:
- 程序员:只需遵循 happens-before 规则,无需理解底层重排序
- JVM:在不违反 happens-before 的前提下,可以自由优化
八大规则:
| 规则 | 说明 | 示例 |
|---|---|---|
| 程序顺序 | 单线程内按代码顺序执行 | a=1; b=2; |
| volatile 规则 | volatile 写 happens-before 后续读 | flag = true; |
| 锁规则 | 解锁 happens-before 后续加锁 | synchronized |
| 线程启动 | start() happens-before 线程操作 | thread.start(); |
| 线程终止 | 线程操作 happens-before join() 返回 | thread.join(); |
| 线程中断 | interrupt() happens-before 检测中断 | thread.interrupt(); |
| 对象终结 | 构造结束 happens-before finalize() | finalize() |
| 传递性 | A→B, B→C ⇒ A→C | 传递链 |
示例:
private volatile boolean flag = false;
private int data = 0;
// 线程 A
public void writer() {
data = 42; // ①
flag = true; // ② volatile 写
}
// 线程 B
public void reader() {
if (flag) { // ③ volatile 读
int x = data; // ④
// x 一定等于 42 ✅
}
}
// happens-before 链:
// ① → ②(程序顺序)
// ② → ③(volatile 规则)
// ③ → ④(程序顺序)
// 结论:① → ④(传递性)
Q6:双重检查锁(DCL)为什么要用 volatile?
参考答案:
问题代码:
public class Singleton {
private static Singleton instance; // ⚠️ 缺少 volatile
public static Singleton getInstance() {
if (instance == null) { // 检查 1
synchronized (Singleton.class) {
if (instance == null) { // 检查 2
instance = new Singleton(); // ⚠️ 问题代码
}
}
}
return instance;
}
}
问题分析:
// instance = new Singleton() 分解为 3 步:
1. memory = allocate(); // 分配内存
2. ctorInstance(memory); // 初始化对象
3. instance = memory; // 设置引用
// ⚠️ 可能重排序为:
1. memory = allocate();
3. instance = memory; // 先设置引用(对象未初始化)
2. ctorInstance(memory); // 后初始化
// 导致:
线程 A 执行到步骤 3 后,instance != null
线程 B 执行检查 1,发现 instance != null,直接返回
线程 B 使用 instance(未初始化的对象)→ NPE
volatile 的作用:
private static volatile Singleton instance; // ✅ 添加 volatile
// volatile 写操作插入 StoreStore 屏障:
memory = allocate();
ctorInstance(memory);
StoreStore 屏障 ← 禁止重排序
instance = memory;
// 保证:读线程看到 instance != null 时,对象一定初始化完成
时序图:
无 volatile:
线程 A 线程 B
──────────────────────────────────────────
分配内存
instance = memory ────────→ if (instance == null) ✗
return instance; ⚠️ 未初始化
初始化对象(太晚了)
有 volatile:
线程 A 线程 B
──────────────────────────────────────────
分配内存
初始化对象
内存屏障
instance = memory ────────→ if (instance == null) ✗
return instance; ✅ 已初始化
Q7:volatile 和 synchronized 有什么区别?
参考答案:
| 对比项 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ 保证 | ✅ 保证 |
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 有序性 | ✅ 禁止重排序 | ✅ 锁机制保证 |
| 性能 | 高(无锁,轻量级) | 低(加锁,重量级) |
| 阻塞 | 不阻塞 | 阻塞 |
| 语义 | 变量级别 | 代码块/方法级别 |
| 锁升级 | 无 | 有(偏向锁→轻量级锁→重量级锁) |
使用场景:
// volatile 适用场景:
// 1. 状态标志
private volatile boolean shutdown = false;
// 2. 单次赋值
private volatile User currentUser;
// 3. 独立观察(一写多读)
private volatile long lastUpdateTime;
// synchronized 适用场景:
// 1. 复合操作
public synchronized void increment() {
count++; // 读-改-写
}
// 2. 多个变量需要保证一致性
public synchronized void transfer(Account from, Account to, int amount) {
from.balance -= amount;
to.balance += amount;
}
// 3. 临界区保护
public void updateCache() {
synchronized (lock) {
cache.clear();
cache.putAll(newData);
}
}
Q8:final 关键字的内存语义是什么?
参考答案:
两大重排序规则:
-
final 域写入规则:
- 禁止将 final 域的写操作重排序到构造函数之外
- 保证:读线程看到对象引用时,final 域一定初始化完成
-
final 域读取规则:
- 禁止初次读取对象引用与读取 final 域重排序
- 保证:读取 final 域前,一定先读取对象引用
实现机制:
public class FinalExample {
private final int x;
private int y;
public FinalExample() {
x = 1; // final 域写入
y = 2; // 普通域写入
// 编译器在此处插入 StoreStore 屏障
} // 构造函数结束
private static FinalExample obj;
public static void writer() {
obj = new FinalExample(); // 对象引用赋值
}
public static void reader() {
FinalExample o = obj; // 读取引用
// 编译器在此处插入 LoadLoad 屏障
int a = o.x; // 读取 final 域,保证读到 1 ✅
int b = o.y; // 读取普通域,可能读到 0 ⚠️
}
}
final 引用类型的特殊保护:
public class FinalReferenceExample {
private final int[] arr;
public FinalReferenceExample() {
arr = new int[10];
arr[0] = 1; // ✅ 这个写入也受 final 保护
// StoreStore 屏障
}
// 保证:读线程看到 arr != null 时,arr[0] 一定等于 1
}
典型应用:
// 不可变对象(String)
public final class String {
private final char[] value; // ✅ 安全发布
public String(String original) {
this.value = original.value;
// 无需额外同步,final 保证可见性
}
}
// 单例模式(静态内部类)
public class Singleton {
private static class Holder {
static final Singleton INSTANCE = new Singleton(); // ✅ 线程安全
}
}
Q9:如何安全地发布对象?
参考答案:
不安全发布示例:
public class UnsafePublish {
private Map<String, String> config;
public UnsafePublish() {
config = new HashMap<>();
config.put("key", "value");
// ⚠️ 另一个线程可能读到:
// 1. config = null(未初始化)
// 2. config != null,但 Map 为空(部分初始化)
}
}
安全发布的四种方式:
方式 1:使用 final 关键字
public class SafePublish1 {
private final Map<String, String> config; // ✅ final 保证可见性
public SafePublish1() {
Map<String, String> temp = new HashMap<>();
temp.put("key", "value");
config = Collections.unmodifiableMap(temp); // 不可变
}
}
方式 2:使用 volatile 关键字
public class SafePublish2 {
private volatile Map<String, String> config; // ✅ volatile 保证可见性
public void init() {
Map<String, String> temp = new HashMap<>();
temp.put("key", "value");
config = temp; // volatile 写
}
}
方式 3:使用 synchronized 同步
public class SafePublish3 {
private Map<String, String> config;
public synchronized void init() { // ✅ 锁保证可见性
config = new HashMap<>();
config.put("key", "value");
}
public synchronized Map<String, String> getConfig() {
return config;
}
}
方式 4:使用静态初始化器
public class SafePublish4 {
// ✅ 类加载时初始化,JVM 保证线程安全
private static final Map<String, String> config = new HashMap<>();
static {
config.put("key", "value");
}
}
总结:
安全发布 = 对象完全初始化 + 引用对其他线程可见
实现方式:
1. final 域(构造函数内初始化)
2. volatile 变量
3. synchronized 保护
4. static 静态初始化
5. 使用并发容器(ConcurrentHashMap 等)
Q10:as-if-serial 和 happens-before 的区别是什么?
参考答案:
as-if-serial(单线程语义)
定义:无论如何重排序,单线程程序的执行结果不能改变。
作用范围:单线程
示例:
int a = 1; // ①
int b = 2; // ②
int c = a + b; // ③
// as-if-serial 允许:
// ② ① ③(交换 ① 和 ②,因为无依赖)
// ① ② ③(保持顺序)
// as-if-serial 禁止:
// ③ ① ②(③ 依赖 ① 和 ②,结果会改变)
关键点:
- 编译器和处理器遵循数据依赖性
- 只保证单线程语义正确
- 多线程下可能出现问题
happens-before(多线程语义)
定义:定义操作的偏序关系,保证多线程下的内存可见性。
作用范围:多线程
示例:
// 线程 A
x = 1; // ①
flag = true; // ② volatile 写
// 线程 B
if (flag) { // ③ volatile 读
int y = x; // ④
}
// happens-before 规则保证:
// ① happens-before ②(程序顺序规则)
// ② happens-before ③(volatile 规则)
// ③ happens-before ④(程序顺序规则)
// 结论:① happens-before ④(y 一定读到 1)
对比表:
| 对比项 | as-if-serial | happens-before |
|---|---|---|
| 适用范围 | 单线程 | 多线程 |
| 保证内容 | 执行结果不变 | 内存可见性 |
| 重排序限制 | 只考虑数据依赖 | 考虑同步关系 |
| 典型场景 | 编译器优化 | volatile、锁、线程操作 |
关系:
as-if-serial 是 happens-before 的基础(程序顺序规则)
happens-before 是 as-if-serial 在多线程下的扩展
九、总结与最佳实践
9.1 内存模型核心要点
JMM 三大特性:
┌─────────────────────────────────────┐
│ 1. 原子性(Atomicity) │
│ - synchronized │
│ - Lock │
│ - Atomic 类 │
├─────────────────────────────────────┤
│ 2. 可见性(Visibility) │
│ - volatile │
│ - synchronized │
│ - final │
├─────────────────────────────────────┤
│ 3. 有序性(Ordering) │
│ - happens-before 规则 │
│ - 内存屏障 │
│ - volatile / synchronized │
└─────────────────────────────────────┘
9.2 并发编程最佳实践
原则 1:优先使用不可变对象
// ✅ 推荐
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
// 无需同步,天然线程安全
public int getX() { return x; }
public int getY() { return y; }
// 修改时返回新对象
public ImmutablePoint move(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}
}
// 优势:
// 1. 无需同步,性能最高
// 2. 可以安全地在多线程间共享
// 3. 可以作为 HashMap 的 key
原则 2:最小化同步范围
// ❌ 不推荐:同步整个方法
public synchronized void process(List<Task> tasks) {
List<Task> validTasks = validate(tasks); // 耗时操作
for (Task task : validTasks) {
execute(task); // 耗时操作
}
updateStatus(); // 只有这一步需要同步
}
// ✅ 推荐:只同步必要的部分
public void process(List<Task> tasks) {
List<Task> validTasks = validate(tasks); // 不需要锁
for (Task task : validTasks) {
execute(task); // 不需要锁
}
synchronized (this) {
updateStatus(); // 只锁定关键区域
}
}
原则 3:优先使用并发工具类
// ❌ 不推荐:手动同步
public class ManualSync {
private final Map<String, User> cache = new HashMap<>();
public synchronized void put(String key, User user) {
cache.put(key, user);
}
public synchronized User get(String key) {
return cache.get(key);
}
}
// ✅ 推荐:使用 ConcurrentHashMap
public class ConcurrentSync {
private final Map<String, User> cache = new ConcurrentHashMap<>();
public void put(String key, User user) {
cache.put(key, user); // 内部使用分段锁,性能更高
}
public User get(String key) {
return cache.get(key); // 读操作无锁
}
}
原则 4:避免在锁内调用外部方法
// ❌ 危险:锁内调用外部方法(可能死锁)
public class DangerousLock {
private final Object lock = new Object();
public void process(Callback callback) {
synchronized (lock) {
// ...
callback.onComplete(); // ⚠️ 外部方法可能再次获取锁
}
}
}
// ✅ 安全:锁外调用外部方法
public class SafeLock {
private final Object lock = new Object();
public void process(Callback callback) {
Object result;
synchronized (lock) {
result = doWork(); // 锁内只处理必要操作
}
callback.onComplete(result); // 锁外调用回调
}
}
原则 5:正确使用 volatile
// ✅ 适合使用 volatile 的场景
// 场景 1:状态标志
public class Server {
private volatile boolean shutdown = false;
public void shutdown() {
shutdown = true; // 单次赋值
}
public void run() {
while (!shutdown) { // 读取最新值
processRequest();
}
}
}
// 场景 2:安全发布
public class ResourceManager {
private volatile Resource resource;
public void init() {
Resource temp = new Resource();
temp.load(); // 完全初始化
resource = temp; // volatile 写,安全发布
}
}
// 场景 3:独立观察
public class Monitor {
private volatile long lastUpdateTime;
public void recordUpdate() {
lastUpdateTime = System.currentTimeMillis(); // 单次写入
}
public boolean isStale() {
return System.currentTimeMillis() - lastUpdateTime > TIMEOUT;
}
}
// ❌ 不适合使用 volatile 的场景
// 场景 1:复合操作
private volatile int count = 0;
public void increment() {
count++; // ❌ 非原子操作,volatile 无法保证
}
// 正确做法:
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // ✅ CAS 原子操作
}
// 场景 2:多变量一致性
private volatile int x;
private volatile int y;
public void update(int newX, int newY) {
x = newX; // ❌ 两个变量的更新不是原子的
y = newY; // ⚠️ 读线程可能看到不一致的状态
}
// 正确做法:
private int x;
private int y;
public synchronized void update(int newX, int newY) {
x = newX; // ✅ 锁保证原子性和可见性
y = newY;
}
9.3 常见并发错误及避免方法
错误 1:双重检查锁忘记 volatile
// ❌ 错误
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // ⚠️ 重排序问题
}
}
}
return instance;
}
}
// ✅ 正确
public class Singleton {
private static volatile Singleton instance; // 添加 volatile
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
错误 2:误用 volatile 数组
// ❌ 错误
public class VolatileArrayError {
private volatile int[] arr = new int[10];
public void update(int index, int value) {
arr[index] = value; // ⚠️ volatile 不保护数组元素
}
}
// ✅ 正确方案 1:使用 AtomicIntegerArray
public class AtomicArrayCorrect {
private AtomicIntegerArray arr = new AtomicIntegerArray(10);
public void update(int index, int value) {
arr.set(index, value); // ✅ 原子操作
}
}
// ✅ 正确方案 2:使用同步
public class SyncArrayCorrect {
private int[] arr = new int[10];
public synchronized void update(int index, int value) {
arr[index] = value; // ✅ 锁保护
}
}
错误 3:读写不对称的同步
// ❌ 错误:写加锁,读不加锁
public class AsymmetricSync {
private int value;
public synchronized void setValue(int value) {
this.value = value; // ✅ 写加锁
}
public int getValue() {
return value; // ❌ 读不加锁,可能读到旧值
}
}
// ✅ 正确方案 1:读写都加锁
public class SymmetricSync {
private int value;
public synchronized void setValue(int value) {
this.value = value;
}
public synchronized int getValue() {
return value; // ✅ 读也加锁
}
}
// ✅ 正确方案 2:使用 volatile
public class VolatileSync {
private volatile int value; // volatile 保证可见性
public void setValue(int value) {
this.value = value; // 无需加锁
}
public int getValue() {
return value; // 无需加锁
}
}
错误 4:final 变量引用可变对象
// ⚠️ 潜在问题
public class FinalMutable {
private final List<String> list = new ArrayList<>();
public FinalMutable() {
list.add("A"); // ✅ 构造函数内,final 保护
}
public void addElement(String s) {
list.add(s); // ⚠️ 构造函数外修改,final 不保护
}
// 其他线程可能读到不一致的 list 状态
}
// ✅ 正确:使用不可变集合
public class FinalImmutable {
private final List<String> list;
public FinalImmutable() {
List<String> temp = new ArrayList<>();
temp.add("A");
list = Collections.unmodifiableList(temp); // 不可变
}
// list 完全线程安全
}
9.4 性能优化技巧
技巧 1:减少锁粒度
// ❌ 粗粒度锁
public class CoarseLock {
private final Map<String, User> cache = new HashMap<>();
public synchronized void put(String key, User user) {
cache.put(key, user); // 整个 Map 加锁
}
}
// ✅ 细粒度锁(使用 ConcurrentHashMap)
public class FineLock {
private final Map<String, User> cache = new ConcurrentHashMap<>();
public void put(String key, User user) {
cache.put(key, user); // 只锁定对应的分段
}
}
// 性能对比:
// 单线程:相差不大
// 多线程:ConcurrentHashMap 提升 3-5 倍
技巧 2:读写分离
// ✅ 使用 ReadWriteLock
public class ReadWriteCache {
private final Map<String, User> cache = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public User get(String key) {
lock.readLock().lock(); // 读锁(允许多个线程)
try {
return cache.get(key);
} finally {
lock.readLock().unlock();
}
}
public void put(String key, User user) {
lock.writeLock().lock(); // 写锁(独占)
try {
cache.put(key, user);
} finally {
lock.writeLock().unlock();
}
}
}
// 适用场景:读多写少(读写比 > 10:1)
技巧 3:使用局部变量
// ❌ 共享变量(需要同步)
public class SharedVariable {
private StringBuilder buffer = new StringBuilder();
public synchronized String process(List<String> data) {
buffer.setLength(0);
for (String s : data) {
buffer.append(s);
}
return buffer.toString();
}
}
// ✅ 局部变量(无需同步)
public class LocalVariable {
public String process(List<String> data) {
StringBuilder buffer = new StringBuilder(); // 线程私有
for (String s : data) {
buffer.append(s);
}
return buffer.toString();
}
}
// 性能提升:10 倍以上(无锁竞争)
技巧 4:延迟初始化
// ✅ Holder 模式(推荐)
public class LazyInit {
private static class Holder {
static final Resource resource = new Resource(); // 类加载时初始化
}
public static Resource getResource() {
return Holder.resource; // 懒加载 + 线程安全 + 高性能
}
}
// 优势:
// 1. 第一次调用时才初始化
// 2. JVM 保证线程安全
// 3. 无锁,性能极高
9.5 并发工具选择指南
┌─────────────────────────────────────────────────────┐
│ 并发场景 工具选择 │
├─────────────────────────────────────────────────────┤
│ 状态标志 volatile boolean │
│ 计数器(低并发) AtomicInteger │
│ 计数器(高并发) LongAdder │
│ 单例模式 静态内部类/枚举 │
│ 缓存 ConcurrentHashMap │
│ 队列 ConcurrentLinkedQueue│
│ 阻塞队列 BlockingQueue │
│ 读多写少 ReadWriteLock │
│ 复杂同步 Semaphore/CountDown │
│ 线程池 ThreadPoolExecutor │
└─────────────────────────────────────────────────────┘
💬 评论