关于volatile_方某人_的博客-程序员秘密

技术标签: Java  java  内存  

转载地址:http://www.jianshu.com/p/dc5a1b80dcfb这里写链接内容

前言
volatile关键字可以说是JAVA比较难理解的一个关键字了,很多书感觉讲的都不太清楚。这篇博客主要梳理一下它的含义,是对自己学习的一个总结,参考了不少资料和博客,希望可以到帮助别人。本文的主要讲一下下面几件事:

  • JAVA内存模型简介
  • volatile的语义:可见性、禁止重排序。
  • 为什么volatile不能保证一致性。
  • volatile的应用场景举例。

    这里写图片描述

Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。由于这种内存模型结构,在多线程情况下会产生很多问题。比如执行如下代码:

i = 8

程序必须先在自己的工作内存中把i进行赋值,然后再将i的值写入到主存中。在这个过程中如果有两个线程A,B均对i进行自增操作,期望得到的值是10,在没有同步机制的情况下可能会有如下的情形发生:A和B同时从主存中读取i,然后分别在自己的工作内存中进行自增操作,然后先后写回主存,则此时主存中的值为9,与我们预期不符。

volatile的语义

可见性
可见性的含义是指:一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。还是之前那个例子:

i = 8

如果i用volatile修饰的话,当有一个线程在主存中读取i,并在自己的工作内存中进行修改的时候,修改后的值会立即强制同步到主存中,并且其他线程中这个值的缓存也都无效。相比之下普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
原理:如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。
但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。

禁止重排序

重排序的含义是:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。这样听着比较拗口,举个例子吧:比如我们要计算23+36+17+44的值,灵活一点的人肯定会这么算(23+17)+(36+44)–>40+80 = 120,而不会按照顺序来算,应为这样进行重排序之后更加便于人脑计算,并且变换顺序之后最终的结果和顺序计算的结果是一致的。总之,重排序就是在保证最后结果一样的情况下,为了处理器的运行效率而对代码执行顺序进行优化的一种操作。
volatile禁止指令重排序的含义:

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行
  • 在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

举例说明:

//x,y为普通变量
//volflag被volatile修饰


x = 10;          //语句1
y = 3;           //语句2
volflag = true;  //语句3
x= 5;           //语句4
y = 9;           //语句5

这个例子中,由于volflag被volatile修饰,所以语句3不会被重排到语句1、语句2前面,也不会被重排到语句4、语句5的后面,但语句1、2和语句4、5的顺序是不能保证的。
另外volatile可以保证在执行到语句3的时候语句1、2是执行完毕的,语句4、5是没有执行的,并且语句1、2的执行结果是对语句4、5是可见的。
原理:Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

为什么volatile不能保证一致性?
首先,我们要知道保证一致性要满足三个条件:原子性,有序性,可见性。

  • 原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:即程序执行的顺序按照代码的先后顺序执行。
    前面已经讲了volatile的可见性和有序性,但它不能保证原子性。下面设想一个情景:
    假如某个时刻变量i的值为10

线程1对变量进行自增操作,线程1先读取了变量i的原始值,然后线程1被阻塞了;
然后线程2对变量进行自增操作,线程2也去读取变量i的原始值,由于线程1只是对变量i进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量i的缓存行无效,也不会导致主存中的值刷新,所以线程2会直接去主存读取inc的值,发现i的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
然后线程1接着进行加1操作,由于已经读取了i的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后i的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,i只增加了1。

可以看出,volatile变量是比锁弱一级的同步机制。当一个线程获取锁之后,别的线程就不能对其进行读取,修改等任何操作,但是获取一个volatile变量之后,只会让该线程对改变量的任何修改对其他线程都可见,但无法阻止其他线程对该变量的执行读取、修改等操作。锁和volatile的对比就好比中国和联合国,有人想干涉中国内政,中国可以发表声明并且同时使用武装力量强制抵挡入侵,但如果是联合国遇到干涉内政的问题就只能发表发表声明了。

volatile的应用场景举例
单例模式中的double check:

class Singleton{
    private volatile static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

为什么要使用volatile 修饰instance?
主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

1.给 instance 分配内存

2.调用 Singleton 的构造函数来初始化成员变量

3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回instance,然后使用,然后理所当然地报错。

参考资料

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/SilentWeek/article/details/77883581

智能推荐

题目:约瑟夫环_夜阑優琿的博客-程序员秘密

题解:这里的约瑟夫环是通过对数组长度的循环位移实现的,注意每次操作后数组长度减一。题中的约瑟夫环每次位移是移三位,但要注意每次位移前都要删除当前元素,所以实际上每次只要移两位就行了。题目:问题 C: 约瑟夫环时间限制: 1 Sec 内存限制: 128 MB提交: 1109 解决: 624[提交][状态][讨论版]题目描述有n个人围成一个圈,从第一个人开始顺序报号1,2,3。凡是报到3退出圈子中的人原来的序号。要求打印出退出人的序号。以及找出最后留在圈子中的人原来的序号。输入

编译原理简明综述笔记_s605133696的博客-程序员秘密_编译原理过程综述

1.1编译相关名词编译程序:源语言为“高级语言”,目标语言为“低级语言”的翻译程序宿主机:运行编译程序的计算机目标机:运行编译程序所产生目标代码的计算机从功能上分诊断编译程序(Diagnostic Compiler):专于帮助程序开发和调试的编译程序优化编译程序(Optimizing Compiler):着重于提高目标代码效率的编译程序交叉编译程序(Cr

c语言用递归方法实现冒泡排序,C语言 冒泡排序 递归法_weixin_39715834的博客-程序员秘密

1 #include 2 #include 3 int main()4 {56 void bubble(int *a,int n);78 int i,n,a[100];910 printf("请输入数组中元素数量(不大于100个):");11 scanf("%d",&n);1213 for(;n<1||n>100;){14 ...

Go 交叉编译x86, arm架构上的Linux可执行程序_叨陪鲤的博客-程序员秘密

1. Golang的交叉编译说明????????????golang中交叉编译非常简单。????????????直接在windows上设置几个Go环境变量便可以编译多种架构多个操作系统的可执行程序。https>go envset GO111MODULE=autoset GOARCH=amd64set GOBIN=set GOCACHE=C:\Users\Admin\AppData\Local\go-buildset GOENV=C:\Users\Admin\AppData\Roamin

雪花ID(Go 实现)_wang豪的博客-程序员秘密_golang 雪花id

雪花ID的实现之前做项目的时候遇到,今天再深入理解一下。文章目录雪花ID的实现前言一、雪花ID的作用?二、雪花ID的原理三、雪花ID的代码实现(Go 语言)四、雪花ID 的缺点前言雪花ID 最由推特开源的一种全局唯一ID 的生成算法,有递增、全局唯一等特性。一、雪花ID的作用?全局的唯一性对于多台机器,毫秒级生成多条不重复ID递增性生成的雪花ID具有递增型,可以加速查询。可用性高支持多线程,分布式的系统架构二、雪花ID的原理最高位是符号位,生成的ID总是正数,

OKHttp以文件形式上传图片_jianning-wu的博客-程序员秘密

AndroidOkhttp框架以文件形式上传图片以Post为例public Observable<String> postImageview(final String url, final MultipartBody requestBody){ //创建被观察者 Observable<String> observable...

随便推点

HTTPS 协议核心原理_爱好学习的青年人的博客-程序员秘密

HTTPS 协议核心原理“安全”的四个特性机密性(加解密)完整性(摘要算法)身份认证(接收方确认身份)不可抵赖(发送方不能否认自己的行为)后两个通过私钥加密,公钥解密 以及 摘要算法实现的数字签名 进行实现对称加密对称加密: 客户端和服务器共享同一个密钥,客户端给服务器发消息时,客户端用此密钥加密,服务器用此密钥解密;反过来,服务器给客户端发消息时,相反的过程。这种加密方式在互联网上有两个问题:**1. 密钥如何传输?**密钥A的传输也需要另外一个密钥B,密钥B的传

分布式数据库一定会替代Oracle吗?_dotNET跨平台的博客-程序员秘密

在传统数据库领域,Oracle一直占据了很大的市场份额,很多企业的业务系统基于此实现OLTP交易场景。近年来,随着分布式技术的发展,分布式数据库逐渐占据了OLTP领域较大的市场,尤其在互联...

使用JSON Server在项目中配置Mock数据以及json server的基本数据操作_无处不楼台的博客-程序员秘密

1、项目根目录下键入:yarn add json-server -D等待安装成功。2、在项目根目录下新建文件夹“__json_server_mock__”.这个文件夹名字前后代两个杠,代表这个文件夹实际与本项目代码关系不大。3、在此新建文件夹中创建文件“db.json”,写入:{ "user":[]}4、再在package.json中找到以下代码,并添加代码:"scripts": { "start": "react-scripts start",

r语言 与python r中python环境的创建_youngleeyoung的博客-程序员秘密

#remotes::install_github(“rstudio/reticulate”)library(reticulate)use_condaenv(‘C:/Users/yll/AppData/Local/r-miniconda/envs/giotto_env/’)‘’’conda_install(envname = “giotto_env” , packages=c(“pandas”, “python-igraph”, “networkx”, “python-louvain”, “leid

数组_学无止境0101的博客-程序员秘密

数组数组的定义数组是相同类型数据的有序集合数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成的其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问他们数组声明创建首先必须声明数组变量,才能在程序中使用数组。下面是声明数组变量的语法:dataType[] arrayRefVar; //首选的方法或dataType arrayReFVar;//效果相同但不是首选方法Java语言使用new操作符来创建数组,语法如下:dataType[] arra

在Linux下编写运行你的第一条代码——Hello Linux_Q-WHai的博客-程序员秘密

最近刚接触Linux,也是在啃那个公认的《鸟哥的Linux私房菜》。是的,的确讲得挺好的。现在看到第10章 vim程序编辑器。虽然鸟哥不太建议我们跳着来阅读他的文章,但是我还是跳了一两个章节没有阅读。别误会,我不是大牛。其实,知道vim也不是学到这一章才了解的。之前是知道的,也用过。不过想在vim中编写C代码的还就是刚刚的想法。于是,我做了一个实验。