关于volatile解决内存可见性问题(保证线程安全)

简介: 关于volatile解决内存可见性问题(保证线程安全)

Volatile是和内存可见性问题是密切相关的。先看下面一段代码,执行结果是什么?

class MyCount{
    public int flag = 0;
}
public class ThreadDemo15 {
    public static void main(String[] args) {
        MyCount myCount = new MyCount();
        Thread t1 = new Thread(()->{
            while (myCount.flag == 0){
                //循环体代码
            }
            System.out.println("t1 线程循环执行结束");
        });
        Thread t2 = new Thread(()->{
            Scanner sc = new Scanner(System.in);
            System.out.println("给flag赋予非0值");
            myCount.flag = sc.nextInt();
        });
        t1.start();
        t2.start();
    }
}

运行结果如下:


我们的预期是t2把flag改成非О的值之后, t1随之就结束循环了。但是,此时无论怎么给flag赋予非零值,t1线程一直处理循环没有结束,也就是说t1线程拿到的flag是0。


这种情况就是一种内存可见性问题,这段代码实际上是有bug的,涉及到一个线程读一个线程修改,是一个线程不安全问题。下面再来探究一下为什么t1线程总是拿到的是0:


8e1050173b474a3cb4a67a302dea189b.png

t1线程这里循环体的判断是要分成两个步骤的,先是load,把内存中flag的值读取到寄存器里,然后再是cmp,把寄存器的值和0进行比较,CPU针对寄存器的操作要比内存操作快3-4个数量级,计算机对于内存的操作比硬盘快3-4个数量级。根据比较结果,决定下一步往哪个地方执行。在t2线程真正修改flag值之前,t1线程的循环已经执行了很多次了,而且t1线程load的结果都是一样的。这里会涉及到编译器的优化问题,由于load是在内存中进行加载,执行的速度太慢了(相对于cmp来说),在加上反复load的结果是一样的,JVM就认为不用再重复的load了,认为flag的值没有修改,认为只读一次就好了,这就是编译器的一种优化方式。

一个线程针对一个变量进行读取操作,同时另一个线程针对这个变量进行修改,此时读到的值不一定就是修改后的值,这个读线程没有感知到变量的变化,这就是一种内存可见性问题。此时需要手动干预,可以给flag这个变量加上volatile关键字,这样每一次的读取操作都要重新读取到这个变量的内存内容,就不会进行优化操作了。

class MyCount{
    volatile public int flag = 0;
}

下面是来自于Java官方文档的对于内存可见性问题的解释:

从JMM(Java内存模型)的角度重新表述内存可见性问题:

Java程序里,有主内存,每个线程还有自己的工作内存。

t1线程进行读取的时候,只是读取了自己工作内存的值。

t2线程进行修改的时候,先修改的自己工作内存的值,然后再把工作内存的内容同步到主内存中。但是由于编译器优化,导致t1没有重新的从主内存同步数据到自己工作内存,读到的结果就是"修改之前”的结果。

在最开始的代码解释中,我只用了内存和寄存器两种概念,但是其实内存和寄存器之间的存储读取的速度差异实在是太大了,在他们中间还有一种高速缓存器cache,因为不同cup的cache不一样,所以应该是为了避免这种差异,统一叫做工作内存。


总结:volatile是不保证原子性的,原子性是靠synchronized来保证的。但是volatile和synchronized都能保证线程安全。在多线程中,针对同一个变量,一个线程进行读取,一个线程进行修改,那么加上volatile关键字可以保证线程安全问题。

 

相关文章
|
2月前
|
存储 安全 Java
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
71 3
|
1月前
|
Java Shell
java中jvm使用jststak定位线程cpu占用内存高的线程
java中jvm使用jststak定位线程cpu占用内存高的线程
11 5
|
7月前
|
Rust 监控 并行计算
用Rust构建电脑网络监控软件:内存安全性和多线程编程
在当今数字化世界中,网络安全一直是至关重要的问题。电脑网络监控软件是确保网络系统安全和高效运行的关键工具。然而,编写电脑网络监控软件需要处理复杂的多线程编程和内存安全性问题。Rust编程语言提供了一种强大的方式来构建安全的电脑网络监控软件,同时避免了许多常见的编程错误。
280 0
|
4月前
|
存储 JSON 运维
【运维】Powershell 服务器系统管理信息总结(进程、线程、磁盘、内存、网络、CPU、持续运行时间、系统账户、日志事件)
【运维】Powershell 服务器系统管理信息总结(进程、线程、磁盘、内存、网络、CPU、持续运行时间、系统账户、日志事件)
52 0
|
4月前
|
存储 缓存 NoSQL
Redis 数据结构+线程模型+持久化+内存淘汰+分布式
Redis 数据结构+线程模型+持久化+内存淘汰+分布式
311 0
|
4月前
|
C++
C++多线程场景中的变量提前释放导致栈内存异常
C++多线程场景中的变量提前释放导致栈内存异常
24 0
|
4月前
|
存储 缓存 Java
volatile 与线程的那些事
volatile 与线程的那些事
22 0
|
4月前
|
设计模式 安全 编译器
线程学习(3)-volatile关键字,wait/notify的使用
线程学习(3)-volatile关键字,wait/notify的使用
27 0
|
4月前
|
缓存
Long包装类型的享元模式注意事项
昨天修复订单接口的bug
30 0
|
5月前
|
安全 Java
7.volatile怎么通过内存屏障保证可见性和有序性?
7.volatile怎么通过内存屏障保证可见性和有序性?
30 0
7.volatile怎么通过内存屏障保证可见性和有序性?