关于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关键字可以保证线程安全问题。

 

相关文章
|
15天前
|
监控 Java 数据库连接
线程池在高并发下如何防止内存泄漏?
线程池在高并发下如何防止内存泄漏?
|
17天前
|
监控 Java 数据库连接
使用线程池时,如何避免内存泄漏的问题?
使用线程池时,如何避免内存泄漏的问题?
|
25天前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
34 2
|
2月前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理
|
17天前
|
监控 数据可视化 Java
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
|
1月前
|
缓存 Java 编译器
【多线程-从零开始-伍】volatile关键字和内存可见性问题
【多线程-从零开始-伍】volatile关键字和内存可见性问题
32 0
|
2月前
|
监控 Java 数据库连接
使用线程池时,如何避免内存泄漏的问题?
使用线程池时,如何避免内存泄漏的问题?
|
2月前
|
Arthas 监控 Java
监控线程池的内存使用情况以预防内存泄漏
监控线程池的内存使用情况以预防内存泄漏
|
2月前
|
监控 数据可视化 Java
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况
|
3月前
|
缓存 Java
【多线程面试题二十三】、 说说你对读写锁的了解volatile关键字有什么用?
这篇文章讨论了Java中的`volatile`关键字,解释了它如何保证变量的可见性和禁止指令重排,以及它不能保证复合操作的原子性。
下一篇
无影云桌面