Java volatile详解

目录


我们知道volatile和synchronized可以解决java中的并发问题,那具体volatile的具体作用是什么呢?内部的原理又是什么?可以解决什么问题,不可以解决什么问题?那我们不妨先来看下并发下的几种性质。

1.并发下的三个性质

1).原子性

原子是指化学反应不可再分的基本微粒,在程序中原子操作就意味着不可分割的操作。这种不可分割在并发时的意义是,此操作在线程中要么完成,要么不完成。不可以在进行中就进行其他操作。
###Java中的原子操作:

int a = 1;
int b = a;
a = b;

###Java中非原子操作产生的并发问题:
这些操作都是原子操作,而下面这种自增操作却不是原子操作

c++;

上述自增操作实际上等价于以下操作:

  1. 在内存中取出c的值
  2. 执行c + 1
  3. 把得到的结果赋值回c

上述过程并不是一个原子操作,而非原子操作就会出现并发问题。
例如:
现有A、B两个线程同时在对共享变量c执行自增操作:

  • 1.A线程执行1操作(从内存中取到c的值)取到的值为0。
  • 2.但在这时切换到了B进程,B进程也执行了1操作,取到的值为0。然后B进程再依次执行2操作和3操作,现在内存中c的值为1.
  • 3.线程切换到了A进程,A进程进行2操作,由于这时的A进程里面的c的值还是0,再进行加1的操作后,值为1。之后执行3操作,内存中的c的值为1.

这样就出现了两个线程一共执行了两次c++,c的值却只增加了1,就出现了错误。

2).可见性

可见性是指一个线程对共享变量所作出的操作,其他的线程都能立即“看见”这种改变。
例如:

// c = 0;
c = 2;

如果共享变量c有可见性:

  1. c的初始值是0。
  2. 一个线程修改了c的值,令其等于2。
  3. 另一个线程马上访问c,读取到的值是2,而不是原来的值0。

3).有序性

JVM虚拟机规范中,如果在单线程条件下,不影响执行的结果,可以改变指令之间的相对顺序以提高性能,那么此条规定就不保证语句的书写顺序执行顺序一致。

a = 2;//1
c = 1;//2
d = a + c;//3

单线程执行条件下,这里 1 和 2 的相对顺序其实对 3 是无影响的,那么就允许虚拟机自主决定 1 和 2 的执行顺序。
执行顺序可以是1-2->3,也可以是2->1->3。

Java内存模型中的天然有序性可以总结为一句话:
在本进程中内观察,一切操作都是有序的;在一个线程观察另一个线程,一切操作都是无序的。
前者是因为“线程内表现为串行语句”,后者是因为“指令重排序“和”工作内存与主存之间同步延迟“;


2.volatile实现的语义

volatile关键字可以实现其中的两点特性:
可见性 和 有序性。

1).volatile实现的可见性

这里写图片描述

工作机制

程序中运行所用到的变量都必须在工作内存中进行读取和修改,每次使用变量之前都从主存中同步一个副本到本线程的工作内存中。

**每个工作内存中的值是对其他工作内存不可以见的。**如果同一个变量在两个或两个以上的工作内存中都存在副本,它无法获知其他的工作内存中是否修改了此值或修改后的值是多少,线程所用的值还是一开始从主存中同步过来的副本,这样就会造成读取的值不一致。
而当工作内存中的值写回到主存中时,由于各个副本的值不一致,就会有一些结果被覆盖掉。

为了解决这个问题,**volatile变量会在每次读取的时候,都把变量的值从主存中同步到当前的工作内存。而每次修改过后,都会立即同步到主存中,这样就保证了各个工作内存中使用的值都是最新的。**从而使在此工作内存中做出的修改,对其他工作内存都立即可见。

实例:

不加volatile关键字会出现死循环的代码:

boolean isRunning = true;

//线程A
while(isRunning){
}
System.out.println("I'm done.");

//线程B
doSometing();
isRunning = false;

线程A和线程B依次启动,那么线程1就有可能进入死循环。
线程A读取到的isRunning的值是从主内存中同步过来的值(有可能在isRunning = false这句话执行之前获取的true)。之后再每次循环时,读取都是当前线程工作内存中的主存变量副本。
所以即使主存中的isRunning的值改变了,线程A依然“看不见”此改变,一直执行下去。

不会出现死循环的代码:

volatile boolean isRunning = true;//用volatile修饰

//线程1
while(isRunning){
}
System.out.println("I'm done.");

//线程2
doSometing();
isRunning = false;

只需要用volatile修饰isRunning就可以解决此问题。
因为线程A每次读取isRunning的值时,都会将从主存中的isRunning重新同步到本地工作内存,并且线程B在修改isRunning值之后,也会立即把更改的值同步到主存中去。isRunning被修改为false之后,线程A下次再从主存中同步并读取时就会“看见”此改变,停止循环。

###还是会出现的问题:
由于volatile并没有保证原子性。如果是在本线程的工作内存中的内存副本从主存同步之后,且未修改工作内存中的副本之前,主存中的值被修改了,那么本线程修改值之后同步到主存就会把原来修改的值覆盖掉。
还是上面的自增的例子:

public void increase(){
	c++//等价于c = c + 1;
}

在内存的交互如下图所示:

其中c代表主存中的变量,c’代表当前线程的工作内存中的变量副本。
Java内存交互操作

其中的各个操作解释如下:

read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用与工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接受到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
(以上摘自《深入理解Java虚拟机(第二版)》P364)

我们发现所有的读取和赋值的操作都是在工作内存中进行的,虽然read、load、use、assign、store、write各自都是原子操作,可这些操作合起来却不是原子操作。也就是说在 ①~⑥之间可以插入其他指令。

让我来们来考虑一种情况:
现有两个线程都在执行increase():

  1. 其中A线程先执行,执行完②之后,线程A的工作内存中的c’值为0;
  2. 然后切换到B线程进行执行,B执行了①~⑥的所有操作,最开始获取的c的值为0,运算后同步到主内存c的值为1;
  3. A线程继续运行,由于A线程工作内存中的c’还是0,那么执行c = c’ + 1运算结果c的值为1。最后执行到⑥写回主内存c的值为1。
    这样就产生了错误。

volatile关键字由于没有原子性,所以在进行需要用到自己本身值的操作时(自增或自减操作等),依然有并发问题,使用时要考虑清楚。

2).volatile实现的有序性

volatile可以禁止指令重排序优化。

boolean isReadComplete = false;
Config config = new Config();

//线程A
config = readConfigFile("sys.config");//1
isReadComplete = true;//2

//线程B
while(!isReadComplete){
	sleep();
}
doSomethingUseConfig(config);

上述代码中,1 和 2 之间没有依赖关系,所以他们的执行相对顺序是不被保证的。指令重排序后,如果执行顺序为2 -> 1,线程B就会拿到一个未初始化的config。

volatile boolean isReadComplete = false;
Config config = new Config();

//线程A
config = readConfigFile("sys.config");//1
isReadComplete = true;//2

//线程B
while(!isReadComplete){
	sleep();
}
doSomethingUseConfig(config);

使用volatile关键字后可以避免此问题,使用volatile修饰之后,在2之前的语句必须在2之前执行,执行的顺序必然是 1 -> 2,这样线程B就会拿到正常初始化的config。

volatile关键字可以保证在用到volatile关键字之前,所有的语句已经执行完毕。在volatile前面的语句还会在volatile前面执行,在volatile后面的语句,依旧在volatile之后执行。

volatile在某些情况下要的性能要优于锁,读取操作与普通的变量几乎没什么区别,但是写操作可能会慢一些,因为它需要在本地代码中插入许多内存屏障来保证处理器不发生乱序执行。但是即使这样,大多数场景下volatile关键字总开销仍然要比锁低。

参考:

  1. 《深入理解Java虚拟机(第二版)》
  2. Java内存模型:http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
  3. Java中volatile关键词的含义:http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html
  4. Java并发编程:volatile关键字解析:http://www.cnblogs.com/dolphin0520/p/3920373.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值