Volatile只会用不知道原理?一篇文章带你深究volatile

简介: 要了解并发编程,首先就需要了解并发编程的三大特性:可见性、原子性和有序性。我们今天要讲的volatile保证了可见性和有序性,但是不保证原子性。接下来会通过几段代码和几张图来强化对volatile的了解。

听说微信搜《Java鱼仔》真的可以变强哦!


(一)概述


要了解并发编程,首先就需要了解并发编程的三大特性:可见性、原子性和有序性。我们今天要讲的volatile保证了可见性和有序性,但是不保证原子性。接下来会通过几段代码和几张图来强化对volatile的了解。


(二)volatile保证可见性


在讲JMM的时候,我们写了一段程序,并知道了两个不同线程之间操作数据是不可见的,即线程B修改了主内存中的共享变量后,线程A是不知道的。这就是线程之间的不可见性。


publicclassTest {
privatestaticbooleanflag=false;
publicstaticvoidmain(String[] args) throwsInterruptedException {
newThread(newRunnable() {
@Overridepublicvoidrun() {
System.out.println("waiting");
while (!flag){}
System.out.println("in");
            }
        }).start();
Thread.sleep(2000);
newThread(newRunnable() {
@Overridepublicvoidrun() {
System.out.println("change flag");
flag=true;
System.out.println("change success");
            }
        }).start();
    }
}

这段代码的结果是第二个线程修改flag的值不会被第一个线程见到


网络异常,图片无法展示
|


现在我们做个小小的改变,给flag加上volatile修饰词


privatestaticvolatilebooleanflag=false;

网络异常,图片无法展示
|


对volatile原理的理解还是需要借助JMM,我们拿上来第一段代码的执行流程图:


网络异常,图片无法展示
|


这是未加volatile时的执行过程,最后会停留在第十步,flag会变成true,但是线程A不知道。


加上volatile后,当主内存的flag被改变时,volatile通过cpu的总线嗅探机制,将其他也正在使用该变量的线程的数据失效掉,使得这些线程要重新读取主内存中的值,最后线程A就发现flag的值被改变了。


(三)Volatile保证有序性


Volatile通过内存屏障禁止指令的重排序,从而保证执行的有序性。具体的内容我在指令重排序和内存屏障的时候讲到了,有兴趣的小伙伴可以看一下。


(四)Volatile不保证原子性


首先还是拿出一段代码,这段代码很简单,定义一个count,并且用volatile修饰。接着创建十个线程,每个线程循环1000次count++ :

publicclassVolatileAtomSample {
privatestaticvolatileintcount=0;
publicstaticvoidmain(String[] args) {
for (inti=0; i<10; i++) {
newThread(newRunnable() {
@Overridepublicvoidrun() {
for (intj=0; j<1000; j++) {
count++;
                    }
                }
            }).start();
        }
try {
Thread.sleep(1000);
        } catch (InterruptedExceptione) {
e.printStackTrace();
        }
System.out.println(count);
    }
}

最后无论执行多少次,你会发现最后输出的count绝大多数都是小于10000,现象已经体现出了volatile不能保证原子性,但是为什么呢?


网络异常,图片无法展示
|


还是通过一个流程图来表示,当线程A执行完count+1后,将值写回到主内存,这个时候由于volatile的可见性,其他工作内存中的count值会被失效掉重新赋值。


可如果线程B刚好执行到第四步呢,线程B工作内存中的count因为volatile变成了1,assign赋值后的count还是等于1,在这里直接少了一次count++。这就是volatile不能保证原子性的原因。


(五)Volatile的使用场景


通过一个很经典的场景来展示一下volatile的应用,双重校验单例:


publicclassSingleton {
privatestaticSingletonInstance;
privateSingleton(){};
publicstaticSingletongetInstance(){
if (Instance==null){
synchronized (Singleton.class){
Instance=newSingleton();
            }
        }
returnInstance;
    }
}

上面这段代码相信大家肯定很熟悉,单例模式最经典的一段代码,获取实例对象时如果为空就初始化,如果不为空就返回实例,看着没有问题,但是在高并发环境下这段代码是会出问题的。 Instance=new Singleton(); 实例化一个对象时,需要经历三步:


网络异常,图片无法展示
|


注意,这三步是有可能发生指令重排序的,因此有可能是先申请内存空间,再把对象赋值到内存里,最后实例化对象。第一步->第三步->第二步的方式来执行。


当此时有两个线程A和B同时申请对象的时候,当线程A执行到重排序后的第二步时


网络异常,图片无法展示
|


线程B执行了if (Instance==null)这行代码,因为此时instance已经赋值到内存里了,所以会直接return Instance; 但是!这个对象并没有被实例化,因此线程B调用该实例时,就报错了。


这个问题的解决办法就是volatile禁止重排序


privatestaticvolatileSingletonInstance;

(六)Volatile可能会导致的问题


凡事都讲究一个度,volatile也是。如果一个程序中用了大量的volatile,就有可能会导致总线风暴,所谓总线风暴,就是指当volatile修饰的变量发生改变时,总线嗅探机制机会将其他内存中的这个变量失效掉,如果volatile很多,无效交互会导致总线带宽达到峰值。因此对volatile的使用也需要适度。



相关文章
|
7月前
|
存储 缓存 Java
volatile底层原理详解
volatile底层原理详解
62 0
|
2月前
|
缓存 Java 编译器
【多线程-从零开始-伍】volatile关键字和内存可见性问题
【多线程-从零开始-伍】volatile关键字和内存可见性问题
42 0
|
7月前
|
存储 缓存 安全
面试官:说说volatile底层实现原理?
面试官:说说volatile底层实现原理?
504 5
面试官:说说volatile底层实现原理?
|
4月前
|
缓存 安全 Java
面试官:说说volatile应用和实现原理?
面试官:说说volatile应用和实现原理?
48 1
|
4月前
|
缓存 Java
【多线程面试题二十三】、 说说你对读写锁的了解volatile关键字有什么用?
这篇文章讨论了Java中的`volatile`关键字,解释了它如何保证变量的可见性和禁止指令重排,以及它不能保证复合操作的原子性。
|
5月前
|
存储 缓存 Java
(一) 玩命死磕Java内存模型(JMM)与 Volatile关键字底层原理
文章的阐述思路为:先阐述`JVM`内存模型、硬件与`OS`(操作系统)内存区域架构、`Java`多线程原理以及`Java`内存模型`JMM`之间的关联关系后,再对`Java`内存模型进行进一步剖析,毕竟许多小伙伴很容易将`Java`内存模型(`JMM`)和`JVM`内存模型的概念相互混淆,本文的目的就是帮助各位彻底理解`JMM`内存模型。
124 0
|
7月前
|
缓存 Java 编译器
volatile原理
volatile原理
58 1
|
存储 缓存 Java
Java内存模型—工作流程、volatile原理
最近在做项目的时候发现很多业务上用到了多线程,通过多线程去提升程序的一个运行效率,借此机会来复盘一下关于并发编程的相关内容。为什么要使用volatile?volatile底层原理是什么?JMM内存模型解决的是什么问题?带着这些问题来分享分享我的成果。
38088 4
|
安全 Java
架构系列——面试必问:volatile的可见性、防止指令重排序以及不能保证原子性的解决方式
架构系列——面试必问:volatile的可见性、防止指令重排序以及不能保证原子性的解决方式
|
缓存 Java 调度
volatile 原理
volatile 原理