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

很早就接触了volatile,可是并无特别深刻的去研究她,只有一个朦胧的概念,就是以为java

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

最近通过查阅各类资料,并结合本身的思考和实践,对volatile有了比较深入的认识,多线程

在此总结并分享给你们。并发

可见性jvm

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

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

 

 这是为何呢?先来看看java的内存模型,以下图:线程

 

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

可见性是指的是线程访问变量是不是最新值。
局部变量不存在可见性问题,而共享内存就会有可见性问题,
由于本地线程在建立的时候,会从主存中读取一个共享变量的副本,且修改也是修改副本,
且并非当即刷新到主存中去,那么其余线程并不会立刻共享变量的修改。
所以,线程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 并不会有锁的特性

相关文章
相关标签/搜索