JUC并发编程(五)volatile/可见性/原子性/有序性->JMM
目录
一 可见性
二 原子性
三 有序性
四 volatile原理
1 如何保证可见性
2 如何保证有序性
3 无法解决指令交错(无法保证原子性)
4 双重检查锁(double-checked locking)
5 happens-before
总体概括
特性 | 含义 | 如何保障 |
---|---|---|
原子性 | 一个或多个操作要么全部执行成功,要么全部失败 | synchronized 、AtomicInteger 等 |
可见性 | 一个线程对共享变量的修改,其他线程能看到 | volatile 、synchronized 、final |
有序性 | 程序执行顺序与代码顺序一致 | volatile 、synchronized 、Lock |
一 可见性
1 问题
代码展示:
package day01.neicun;public class example1 {static boolean flag = true;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (flag) {}});t1.start();// 这里释放休眠,防止t1线程还未执行就被主线程给修改变量try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}flag = false;}
}
2 解决
- 1 volatile易变关键字
它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存当中查找变量的值,必须到主存当中获取他的值,线程操作volatile变量都是直接操作主存。
线程不会再从缓存当中获取volatile的值,而是从主存当中获取,效率会有所下降,但是保证了共享变量在多个线程之间的可见性。
public class example1 {volatile static boolean flag = true;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (flag) {}});t1.start();// 这里释放休眠,防止t1线程还未执行就被主线程给修改变量try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}flag = false;}
}
- 2 使用synchroized
加锁(如 synchronized
或 ReentrantLock
)会强制线程从主内存中读取变量的最新值,而不是从本地缓存(工作内存)中读取。这是 Java 内存模型(JMM)保证可见性的核心机制之一。
package day01.neicun;public class example1 {static boolean flag = true;// 锁对象final static Object obj = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {//这里会在执行完判断后将锁释放,那修改变量的同步代码块就可以运行了while (true) {synchronized (obj) {if (flag) {break;}}}});t1.start();// 这里释放休眠,防止t1线程还未执行就被主线程给修改变量try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (obj){flag = false;}}
}
二 原子性
机制 | 原理 | 适用场景 | 特点 |
---|---|---|---|
synchronized | 互斥锁 | 需要保护临界区代码块或方法 | JVM内置,简单易用,可能阻塞 |
显式锁 (Lock ) | 更灵活的互斥锁 | 需要高级锁特性(可中断、公平等) | 更灵活,需手动释放锁 |
原子类 | CAS + volatile | 对单个变量进行原子操作 | 无锁,高性能 |
不可变对象 | 状态不可修改 | 共享只读数据 | 无同步开销,最安全 |
三 有序性
有序性是指:程序执行的顺序按照代码的先后顺序执行。
1 指令重排
指令重排(Instruction Reordering)是现代CPU和编译器为了提高程序执行效率而采用的关键优化技术。它通过重新排列指令的执行顺序来最大化利用CPU资源,但可能引发多线程环境下的可见性问题。
现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令。
1 使用volatile的情况
public class ConcurrencyTest {int num = 0;volatile boolean ready = false;public void actor1(I_Result r) {if (ready) { // 读取 volatile 变量,确保看到最新的值r.r1 = num + num;} else {r.r1 = 1;}}public void actor2(I_Result r) {num = 2; // 写操作ready = true; // 写 volatile 变量,确保前面的操作已完成}
}
四 volatile原理
volatile
关键字的底层原理是 内存屏障(Memory Barriers) 机制。
volatile
解决的是 "一个线程写,其他线程读" 时的有序性和可见性问题,而解决 "多个线程同时写" 需要更强的同步机制。
- 对volatile变量的写指令后会加入写屏障
- 对volatile变量的读指令前会加入读屏障
总的来说写屏障保障之前的写and重排,读屏障保障之后的读and重排
1 如何保证可见性
- 写屏障保证在该屏障之前的,对变量的改动都同步到主存当中。
- 读屏障保证在该屏障之后对变量的读取,加载的是主存中的最新数据。
2 如何保证有序性
- 写屏障会确保指令重排序时,不会将屏障之前的代码排在写屏障之后。
- 读屏障会确保指令重排序时,不会将屏障之后的代码排在读屏障之前。
3 无法解决指令交错(无法保证原子性)
volatile
解决的是 "一个线程写,其他线程读" 时的有序性和可见性问题,而解决 "多个线程同时写" 需要更强的同步机制(结合锁机制确保)。
- 写屏障仅仅是保证之后的读取能读取到最新的结果,但是不能保证读跑到他前面去。
- 而有序性的保证也只是保证了本线程内相关代码不被重排序。
4 双重检查锁
双重检查锁(double-checked locking)的核心目的---以单例为例
1. 双重检查的作用
双重检查锁(Double-Checked Locking)的核心目的是:
- 性能优化:第一次检查避免不必要的同步开销。
- 线程安全:第二次检查确保在同步块内只有一个线程能创建实例。
代码示例:
public final class Singleton {// 1. volatile 修饰的静态实例private static volatile Singleton instance;// 2. 私有构造函数private Singleton() {}// 3. 双重检查的获取实例方法public static Singleton getInstance() {if (instance == null) { // 第一次检查(无锁)synchronized (Singleton.class) { // 同步块if (instance == null) { // 第二次检查(有锁)instance = new Singleton(); // 创建实例}}}return instance;}
}
一些知识点
-
final
修饰类的原因
防止子类化破坏单例性,确保全局唯一性。 -
实例变量
private
的原因
强制通过工厂方法访问,防止外部直接修改实例状态。(private为类内部) -
实例变量
volatile
的原因
禁止指令重排序(解决部分构造问题),保证跨线程可见性。 -
构造函数
private
的原因
禁止外部实例化,确保单例控制权唯一。 -
两次
null
判断的原因
首次检查(无锁)避免性能开销;二次检查(同步块内)防止重复创建。 -
类对象加锁的原因
类锁保证全局唯一性,且独立于实例生命周期,避免死锁风险。 -
类的实现目的
实现高性能线程安全单例:延迟加载、双重检查锁定、全局唯一实例访问。
破坏单例模式
1 反射方式
通过反射强行调用私有构造函数(setAccessible(true)
),绕过单例的静态实例控制逻辑,直接创建新对象。
2 反序列化
反序列化时,ObjectInputStream
默认通过反射调用无参构造器新建对象,而非返回单例的静态实例。
普通方式解决
package day01.vola;import java.io.Serial;
import java.io.Serializable;public final class Singleton implements Serializable {// 1. volatile 修饰的静态实例private static volatile Singleton instance;// 2. 私有构造函数private Singleton() {// 防止反射破坏单例if (instance != null) {throw new RuntimeException("单例类禁止通过反射创建实例!");}}// 3. 双重检查的获取实例方法public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}// 4. 防止反序列化破坏单例@Serialprivate Object readResolve(){return instance;}
}
枚举方式解决
为何可以实现单例并解决反序列化和反射而导致的问题
-
实例数量固定:枚举在编译期确定所有实例,运行时无法创建新实例
-
类加载机制:枚举实例在首次访问时由 JVM 静态初始化(线程安全)
-
JVM 底层保障:
-
禁止反射创建枚举实例(
Constructor.newInstance()
直接抛出异常) -
特殊序列化机制(仅存储枚举名,反序列化通过
Enum.valueOf()
还原)
-
-
语法限制:
-
枚举构造器强制私有化
-
不能显式实例化(编译器阻止
new
操作)
-
代码实现:
package day01.vola;public enum Single {// 唯一单例实例(名称可自定义)INSTANCE;// 单例状态字段(自动线程安全)private int requestCount = 0;// 单例业务方法public void handleRequest() {requestCount++;System.out.println("Handled request #" + requestCount);}// 可添加静态业务方法public static void warmUp() {System.out.println("Singleton initialized");}
}
静态内部类实现单例
5 happens-before
happens-before规定了对共享变量的写操作对其他线程的读操作可见,他是可见性与有序性的一套规则总结。
八大规则
规则 | 示例 | 作用 |
---|---|---|
程序顺序规则 | x=1; y=2; → x 先于 y | 单线程操作顺序保留 |
监视器锁规则 | 解锁 → 后续加锁 | synchronized 可见性保证 |
volatile规则 | 写 volatile → 后续读 | 禁止重排序 + 跨线程可见 |
线程启动规则 | thread.start() → 线程内操作 | 父线程配置对子线程可见 |
线程终止规则 | 线程结束 → thread.join() | 子线程操作对父线程可见 |
传递性规则 | A→B 且 B→C ⇒ A→C | 跨操作链式可见 |
中断规则 | interrupt() → 检测到中断 | 中断信号可靠传递 |
finalize规则 | 构造结束 → finalize() | 对象完整构造后才回收 |