volatile
不可见性问题示例1
public class Test {
// 加 volatile 就能解决
public static Integer flag = 1;
public static void main(String[] args) {
// 线程 A 死循环,当 a == 1 时不会停止,当 a != 1 时按理来说就会停止
new Thread(() -> {
while (flag == 1){
}
}, "线程A").start();
// 线程 B 两秒后修改 a 的值
new Thread(() -> {
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2));
flag = 2;
}, "线程B").start();
}
}
2 秒后,线程 B 修改变量 a 的值,如果线程 A 能及时知道 a 的值已变化就会停止,实际情况是不会停止的
不可见性问题示例2
经典的双重检查案例
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
// 私有构造函数防止外部实例化
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
如果不加 volatile,有可能导致问题,具体分析下导致什么问题,什么原因导致的,以及为什么加了 volatile 就能解决问题
首先要明白 new 的过程分为三个步骤:分配内存、初始化属性、地址引用
线程A | 线程B | |
---|---|---|
第一次检查是 null | 第一次检查也是 null | 第一次没做同步,两个线程可能同时进入 |
拿到锁,进入同步块 | 拿不到锁,等待 | |
第二次检查,还是 null | ||
开始创建对象 | ||
释放锁 | 拿到锁,进入同步块 | |
回写主存 | 第二次检查,还是 null | 因为线程 A 还没回写完主存,其实对象已经创建了,但是线程 B 不可见 要是有 volatile 修饰,线程 A 的修改对于线程 B 就是立即可见的,就可以避免问题 |
开始创建对象 | ||
释放锁 | ||
回写主存 |
volatile 原理
可见性
可以理解为,每个线程不是读取自己的工作内存,而是直接读取主内存,每次修改都会立即回写主内存(也可以理解为当更新后别的线程的副本变量会失效,会强制别的线程重新读取最新的值),更深入的原因是缓存一致性,官方描述如下(不是很理解,后面再看):
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取
有序性
java 代码最终翻译成可执行的机器指令会被优化、或者只能保证赋值操作啥的是准确的,但是不一定就是我们写的代码的顺序,这就是 指令重排序,底层使用内存屏障来完成的,对于 volatile 完整的情况应该是这样的(不是很理解,后面再看)
- 在每一个volatile的写(store)之前,加入一个StoreStore屏障和一个LoadStore屏障
- 在每一个volatile的写(store)之后,加入一个StoreLoad屏障和一个StroeStore屏障
- 在每一个volatile的读(load)之后,加一个LoadLoad屏障和LoadStrore屏障
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具