深入解析 Java 内存可见性问题:从现象到 volatile 解决方案

前面我们提到内存可见性问题也是引起线程安全的一个原因,本文我们就来详细说一下什么是内存可见性问题。

引入问题

java 复制代码
import java.util.Scanner;

public class Demo6 {
    private static int flag=0;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while (flag==0){

            }
            System.out.println("t1线程结束");
        });
        Thread t2=new Thread(()->{
           //该线程针对flag进行修改
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入flag的值");
            flag= scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

由运行结果可以看出,我们输入了一个非0值,但是t1线程并没有结束,所以这也是一个线程安全问题

出现这一现象的原因就是内存可见性问题。

这是因为研究JDK的大佬们,希望通过编译器或者JVM对程序员写的代码进行优化。

编译器或者JVM会在我们原有代码逻辑不变的情况下,对我们的代码进行调整,让我们代码的执行效率提高。

但是虽然编译器声称会让原代码的执行逻辑不变,但在多线程中,编译器的判断可能会失误。

这就导致可能因为编译器的优化,让优化后的逻辑和优化前的逻辑在一些细节上会有些偏差 ~

那么这时就会出现一些线程安全问题。

分析问题

在上述循环中,flag==0这一判断在计算机中对应的是一个cmp这样的指令。

而一个cmp指令执行之前又要先进行load (读内存操作)指令。

这里要注意的是load的时间开销可能是cmp的几千倍。

而flag的值的修改是在t2线程中等待用户输入的,由于我们并不确定用户多长时间后才修改flag的值,那么这段时间内,JVM就会误以为没人修改flag的数值,flag的数值始终是0。

那么此时,就把读内存的操作优化为了读寄存器的操作

(即把内存中的值读取到寄存器中,后续再进行load操作之后,就不从内存中读取了,直接从寄存器中读取)

那么等到很多秒之后,再修改flag的数值,此时t1线程就感知不到了。
由此可见,编译器优化,使得t1线程的读取操作,不是真正读内存。

调整代码

接下来我们对上述代码的循环内部稍作调整

java 复制代码
 while (flag==0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                }
            }

这次运行结果就是符合我们的预期的,这是什么原因呢?

这是因为当我们加入sleep(1)操作之后就让上面的循环次数大幅度减少了,而且sleep消耗的时间要比load和cmp多得多,那么此时优不优化就无足轻重了。

因此,这次的运行结果是符合我们的预期的。

解决问题

上述问题在编译器优化的角度是难以进行调整的,于是我们在语法中引入了volatile关键字用来解决上述问题(该关键字只能用来修饰变量

话不多说,上代码~~~

java 复制代码
 private static volatile int flag=0;

其实很简单,我们只需要在flag前面加上这个关键字即可,此时我们再执行这个代码,运行结果就符合我们的预期了~~~

这是因为 volatile 关键字会强制要求:每次读取该变量时,都必须从内存中重新获取最新值,而不能使用 CPU 寄存器中的数值;每次修改该变量后,也必须立即将新值同步回内存,确保其他线程能 "看到" 最新的变量状态。

结语

通过本文的案例分析,我们不仅搞懂了内存可见性问题的本质 ------ 编译器 / JVM 优化导致线程读取不到变量的最新内存状态,还掌握了最直接的解决方案:用 volatile 关键字修饰变量

在实际开发中,当多个线程涉及同一变量的读写操作时,及时使用 volatile,能有效避免因内存可见性引发的线程安全隐患。

相关推荐
心平气和量大福大7 小时前
C#-WPF-UserControl-生命周期(加载 退出)
开发语言·c#·wpf
小龙报7 小时前
【优选算法】1. 水果成蓝 2.找到字符串中所有字母的异位词
java·c语言·数据结构·数据库·c++·redis·算法
sunywz7 小时前
【c#】 Web Deploy一键发布,IIS部署全流程
开发语言·前端·c#
自强的小白7 小时前
重点!起始时间和结束时间与创建时间和修改时间的区别(localdate与LocaldateTIME区别)以及增删改查什么时候设置目前时间(添加和更新员工时)
java
Brilliantwxx7 小时前
【Linux】 软件包管理器(yum)+ Vim使用
linux·运维·服务器·开发语言·编辑器·vim
雪的季节7 小时前
Python 线程同步与异步编程 全解析
java·开发语言
吃饱了得干活7 小时前
缓存与数据库一致性:从理论到实战
java·后端·面试
520拼好饭被践踏8 小时前
JAVA+Agent学习day22
java·开发语言·后端·学习
笨蛋不要掉眼泪8 小时前
Java虚拟机:堆的参数配置
java·开发语言·jvm
霸道流氓气质8 小时前
SpringBoot中使用JasperReports 报表引擎 — 介绍、原理与使用实践
java·spring boot·后端