博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Volatile
阅读量:7145 次
发布时间:2019-06-29

本文共 1984 字,大约阅读时间需要 6 分钟。

hot3.png

很早就接触了volatile,但是并没有特别深入的去研究她,只有一个朦胧的概念,就是觉得用她来解决可见性的,但可见性又是什么呢?

最近经过查阅各种资料,并结合自己的思考和实践,对volatile有了比较深刻的认识,在此总结并分享给大家。

可见性:

    如何理解可见性,还是来看个会出现死循环的例子:

        173213_wxYr_3274018.png

 

 

173227_RS35_3274018.png

111145_P7dK_3274018.png

程序一直在运行,不会停止,这是为什么呢?

 

这是为什么呢,先来看看java的内存模型:

173311_7zSt_3274018.png

java内存分为工作内存和主存

        工作内存:即java线程的本地内存,是单独给某个线程分配的,存储局部变量等,同时也会复制主存的共享变量作为本地的副本,目的是为了减少和主存通信的频率,提高效率。

        主存:存储类成员变量等。

        可见性是指的是线程访问变量是否是最新值。局部变量不存在可见性问题,而共享内存就会有可见性问题,因为本地线程在创建的时候,会从主存中读取一个共享变量的副本,且修改也是修改副本,且并不是立即刷新到主存中去,那么其他线程并不会马上共享变量的修改。 因此,线程B修改共享变量后,线程A并不会马上知晓,就会出现上述死循环的问题。

        解决共享变量可见性问题,需要用volatile关键字修饰。如下图代码就不会出现死循环:

 

173544_dfk5_3274018.png

那么为什么能解决死循环的问题呢?

    可见性的特性总结为以下2点:
        1. 对volatile变量的写会立即刷新到主存
        2. 对volatile变量的读会读主存中的新值
    可以用如下图更清晰的描述: 

173640_RMTj_3274018.png

如此一来,就不会出现死循环了。

    为了能更深刻的理解volatile的语义,我们来看下面的时序图,回答这2个问题:

173705_Eict_3274018.png

问题1:t2时刻,如果线程A读取running变量,会读取到false,还是等待线程B执行完呢?

        答案是否定的,volatile并没有锁的特性。
问题2:t4时刻,线程A是否一定能读取到线程B修改后的最新值
        答案是肯定的,线程A会从重新从主存中读取running的最新值。

 

volatile变量的原子性

    我看了很多文章,有些文章甚至是出版的书籍都说volatile不是原子的,
他们举的例子是i++操作,i++本身不是原子操作,是读并写,我这里要讲的原子性
指的是写操作,原子性的特别总结为2点:
        1. 对一个volatile变量的写操作,只有所有步骤完成,才能被其它线程读取到。
        2. 多个线程对volatile变量的写操作本质上是有先后顺序的。也就是说并发写没有问题。
    这样说也许读者感觉不到和非volatile变量有什么区别,我来举个例子:

//线程1初始化User

User user;
user = new User();
//线程2读取user
if(user!=null){
user.getName();
}

在多线程并发环境下,线程2读取到的user可能未初始化完成

具体来看User user = new User的语义:
1:分配对象的内存空间
2:初始化对线
3:设置user指向刚分配的内存地址
步骤2和步骤3可能会被重排序,流程变为
1->3->2
这些线程1在执行完第3步而还没来得及执行完第2步的时候,如果内存刷新到了主存,
那么线程2将得到一个未初始化完成的对象。因此如果将user声明为volatile的,那么步骤2,3
将不会被重排序。

下面我们来看一个具体案例,一个基于双重检查的懒加载的单例模式实现:

174438_yzd6_3274018.png

    这个单例模式看起来很完美,如果instance为空,则加锁,只有一个线程进入同步块完成对象的初始化,然后instance不为空,那么后续的所有线程获取instance都不用加锁,从而提升了性能。但是我们刚才讲了对象赋值操作步骤可能会存在重排序,即当前线程的步骤4执行到一半,其它线程如果进来执行到步骤1,instance已经不为null,因此将会读取到一个没有初始化完成的对象。但如果将instance用volatile来修饰,就完全不一样了,对instance的写入操作将会变成一个原子操作,没有初始化完,就不会被刷新到主存中。

修改后的单例模式代码如下:

174516_3ARu_3274018.png

对volatile理解的误区

很多人会认为对volatile变量的所有操作都是原子性的,比如自增i++

这其实是不对的。

看如下代码:

174537_FlX1_3274018.png

如果i++的操作是线程安全的,那么预期结果应该是i=20000

然而运行的结果是:11349

说明i++存在并发问题
i++语义是i=i+1
分为2个步骤
步骤1:读取i=0
步骤2:计算i+1=1,并重新赋值给i
那么可能存在2个线程同时读取到i=0,并计算出结果i=1然后赋值给i
那么就得不到预期结果i=2。
这个问题说明了2个问题:
1.i++这种操作不是原子操作
2.volatile 并不会有锁的特性

 

 

转载于:https://my.oschina.net/hcy8888/blog/1647347

你可能感兴趣的文章
HDU sum问题
查看>>
C语言基础知识汇总
查看>>
数字高程模型和地图——thematicmapping.org译文(一)
查看>>
8-5 泛型类型擦除
查看>>
正文处理命令及tar命令
查看>>
实习第三周小记-----生活在于经历 分类: 程序人生 ...
查看>>
将excel中的数据转为json格式
查看>>
字典操作
查看>>
[洛谷P2839][国家集训队]middle
查看>>
《求一个数组的连续的最大子数组之和》
查看>>
设置行间距,自适应文字大小
查看>>
资金流学习-广州发展
查看>>
python基础3(元祖、字典、深浅copy、集合、文件处理)
查看>>
正确编写Designated Initializer的几个原则
查看>>
iOS播放动态GIF图片
查看>>
获取版本号
查看>>
使用jdk自带的visualVM监控远程监控was
查看>>
集合视图UICollectionView 介绍及其示例程序
查看>>
JsLint 的安装和使用
查看>>
合并傻子//区间dp
查看>>