并发编程的本质:通过合理的机制(锁、原子类、内存屏障等)协调多线程对共享资源的访问,确保三性不被破坏。
所以实际上JMM只是Java为了解决并发问题引入的一种模型(规范),提供的一种结构化的解决方案。
JMM是共享内存模型,提供了三性问题的解决办法
一、并发三性的定义与重要性
特性 | 定义 | 问题场景 | 解决方案示例 |
---|---|---|---|
原子性 | 一个操作是不可分割的,要么全部执行成功,要么完全不执行。 | 多线程同时修改共享变量(如 i++ ),导致结果不一致。 |
synchronized 、Lock 、原子类(AtomicInteger ) |
可见性 | 一个线程修改共享变量后,其他线程能立即看到修改后的值。 | 线程 A 修改了变量,但线程 B 仍读到旧值(如循环检测标志位时死循环)。 | volatile 、synchronized 、Lock |
有序性 | 程序执行的顺序符合代码的先后逻辑,避免编译器和 CPU 的指令重排序导致意外结果。 | 线程 A 先执行写操作 a=1 ,再写 flag=true ,但线程 B 可能先看到 flag=true ,后看到 a=0 。 |
volatile 、synchronized 、final 、happens-before 规则 |
二、三性之间的关系
- 原子性是基础 :
若操作不具备原子性(如非原子类的i++
),多线程并发时必然导致数据不一致。 - 可见性依赖原子性 :
即使操作是原子的,若修改不可见(如未用volatile
),其他线程仍可能读取旧值。 - 有序性影响可见性 :
指令重排序可能导致共享变量的修改顺序对其他线程不可见(如双重检查锁单例模式需用volatile
)。
三、如何保证并发三性
1. 原子性的保证
- 锁机制 :
synchronized
、ReentrantLock
等。 - 无锁编程 :原子类(
AtomicInteger
)、LongAdder
、CAS
。 - 不可变对象 :
final
修饰的不可变对象天然线程安全。
2. 可见性的保证
volatile
关键字:强制读写直接操作主内存。- 锁机制:锁的释放会强制同步工作内存到主内存,锁的获取会强制从主内存加载。
final
关键字 :正确构造的final
字段对其他线程可见。
3. 有序性的保证
volatile
关键字:禁止指令重排序(插入内存屏障)。synchronized
/Lock
:同步块内的代码不会被重排序到块外。happens-before
规则:JMM 定义的顺序性约束(如线程启动规则、传递性规则等)。
四、实际场景分析
1. 单例模式(双重检查锁)
java
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;
}
}
- 问题 :若不用
volatile
,new Singleton()
的指令可能重排序(先分配内存地址,再初始化对象),导致其他线程拿到未初始化的对象。 - 解决 :
volatile
禁止重排序,保证有序性。
2. 高并发计数器
java
public class Counter {
private LongAdder count = new LongAdder(); // 高并发下性能优于 AtomicLong
public void increment() {
count.increment(); // 原子性 + 可见性
}
public long get() {
return count.sum();
}
}
- 原理 :
LongAdder
通过分段 CAS 减少竞争,保证原子性和可见性。
五、总结
维度 | 核心问题 | 解决手段 | 工具/关键字 |
---|---|---|---|
原子性 | 操作不可分割 | 锁、原子类、CAS | synchronized 、AtomicInteger 、LongAdder |
可见性 | 修改后立即可见 | 强制主内存同步 | volatile 、synchronized 、Lock |
有序性 | 指令按预期顺序执行 | 禁止重排序、happens-before 规则 | volatile 、final 、synchronized |