你真的了解volatile吗,关于volatile的那些事

转载 2018年04月16日 18:26:37

原文链接:https://www.cnblogs.com/tangyanbo/p/6538488.html;

很早就接触了volatile,但是并没有特别深入的去研究她,只有一个朦胧的概念,就是觉得

用她来解决可见性的,但可见性又是什么呢?

最近经过查阅各种资料,并结合自己的思考和实践,对volatile有了比较深刻的认识,

在此总结并分享给大家。

可见性

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

(注意:运行时请加上jvm参数:-server,while循环内不要有标准输出):

 

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

 

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

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

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

 

那么为什么能解决死循环的问题呢?
可见性的特性总结为以下2点:
1. 对volatile变量的写会立即刷新到主存
2. 对volatile变量的读会读主存中的新值
可以用如下图更清晰的描述: 

 

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

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

问题1:t2时刻,如果线程A读取running变量,会读取到false,还是等待线程B执行完呢?
答案是否定的,volatile并没有锁的特性。
问题2:t4时刻,线程A是否一定能读取到线程B修改后的最新值
答案是肯定的,线程A会从重新从主存中读取running的最新值。


还有一种办法也可以解决死循环的问题:

虽然running变量上没有volatile关键字修饰,但是读和写running都是同步方法

同步块存在如下语义:
1.进入同步块,访问共享变量会去读取主存
2.退出同步块,本地内存对共享变量的修改会立即刷新到主存
因此上述代码不会出现死循环。


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
将不会被重排序。
下面我们来看一个具体案例,一个基于双重检查的懒加载的单例模式实现:

 

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

 

对volatile理解的误区

很多人会认为对volatile变量的所有操作都是原子性的,比如自增i++
这其实是不对的。
看如下代码:

如果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 并不会有锁的特性



Git中的那些事——Egit插件管理

-
  • 1970年01月01日 08:00

你真的了解volatile关键字吗?

一、Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的。 Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存...
  • u013322876
  • u013322876
  • 2017-04-03 12:33:54
  • 148

面试必问的volatile,你了解多少?

前言 Java中volatile这个热门的关键字,在面试中经常会被提及,在各种技术交流群中也经常被讨论,但似乎讨论不出一个完美的结果,带着种种疑惑,准备从JVM、C++、汇编的角度重新梳理一遍。 ...
  • qq_41534566
  • qq_41534566
  • 2018-01-27 16:27:12
  • 101

你真的了解volatile吗?

volatile关键字经常在并发编程中使用,其特性是保证可见性以及有序性,但是关于volatile的使用仍然要小心,这需要明白volatile关键字的特性及实现的原理,这也是本篇文章的主要内容。 一、...
  • Mr_sunrise
  • Mr_sunrise
  • 2017-07-21 17:01:18
  • 154

有volatile在,变量一定会安全吗?

在编程中,安全问题一直是我们关注的重点,能否确保程序在多线程的情况下实现安全,这应该一直是各位攻城狮们极其在意的。今天小编就和大家一起接着上篇博客继续聊一聊 关于原子性和可见性的问题! ✎  什么是...
  • YSC1123
  • YSC1123
  • 2017-09-08 14:27:40
  • 334

Java中有关volatile的几个小面试题

码农小二哥 2017-05-10 16:01 1)Java 中能创建 volatile 数组吗? 能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整...
  • u011277123
  • u011277123
  • 2017-05-16 09:58:18
  • 2608

Volatile真的能解决线程并发吗?

今天看见有一兄弟用的Volatile 修饰的变量 来保证系统中此值的唯一性,以前我也用过volatile 个人认为这样不能保证在线程并发的情况 值的正确性。       用volatile修饰的变量 ...
  • cainiaoxiaozhou
  • cainiaoxiaozhou
  • 2015-09-28 12:40:48
  • 1078

volatile的陷阱

最近写的关于在嵌入式开发中常遇到的关于volatile关键字使用的短文,都是些通用的技术,贴上来share。  对于volatile关键字,大部分的C语言教材都是一笔带过,并没有做太过深入的分析,所以...
  • luoqindong
  • luoqindong
  • 2015-02-15 09:53:31
  • 1496

JSON的那些事儿(你真的了解JSON吗)

前言差不多一周没有写博客,这篇博客的内容是自己前几天就看的了,按照道理应该几天前就分享出来,不过最近因为准备期末考试的原因,也耽误了,今天趁着周六,难得的睡到8点多起床,在这里总结一些这篇文章,迎接这...
  • m0_37568521
  • m0_37568521
  • 2017-12-09 11:41:51
  • 127

关于volatile的一些问题

Q:什么是可见性? A:可见性是一个线程改变值得时候 如果不用volatile对其他线程是不可见的Q:volatile可以保证线程安全么? A:不安全,volatile只能保证可见性,不能保证原子...
  • u012841509
  • u012841509
  • 2016-11-03 10:08:47
  • 360
收藏助手
不良信息举报
您举报文章:你真的了解volatile吗,关于volatile的那些事
举报原因:
原因补充:

(最多只允许输入30个字)