您的位置:首页 > 编程语言 > Java开发

java volatile关键字总结

2015-07-07 11:15 260 查看
之前就看过很多关于volatile的资料,本文是作者对volatile关键字的一些总结,在这里先感谢《java内存模型》的作者程晓明。

目录

目录

java关键字volatile总结
线程的可见性

指令重排序

java关键字volatile总结

关于volatile修饰的变量,虚拟机做出如下保证:

线程的可见性

禁止指令的重排序

线程的可见性

java内存模型(简称JMM)规定了所有的变量都存储在主存中,每个线程都有自己的工作内存,工作内存中保存了主存中对应变量的拷贝,对变量的修改是在工作内存中完成,然后同步至主存中。JMM模型如图:



由上述可以得出,多个线程对主存中同一普通变量的修改,是存在”可见性”问题的,也就是指在一个线程中对变量修改后,其他线程不一定及时知道。而虚拟机会保证对于volatile的变量,修改是对其他线程立即可见的。那么虚拟机是如何做到这一点的呢?

在JMM中定义了八种操作来实现工作内存与主存的交互,这些操作都是原子操作,期间不会发生其他的线程切换:

Lock:将主存中的变量标记为一条线程独占状态;

Unlock:将锁定的变量释放;

Read:将主存中的变量传输到工作内存中;

Load:把read操作接收到的变量值放入工作内存的变量副本中;

Use:把工作内存中的值传递给执行引擎;

Assign:把从执行引擎中接收到的值赋值给工作内存中的变量;

Store:把工作内存中的变量传递至主存;

Write:将store接收到的变量的值赋值给主存中的变量;

在虚拟机中,对于volatile有如下规则,假设T表示一个线程,P和Q表示两个volatile变量,在进行上面描述的操作时:

只有当T对P执行的前一个动作是load时,T才能对P执行use动作,并且只有T对P执行的后一个动作是use时,T才能对P进行load操作;这样就保证执行引擎每次在使用变量之前,都会从主存中读取最新的值。

只有当T对P执行的前一个动作是assign时,T才能对P进行store操作,并且只有T对P执行的后一个动作是store时,T才能对P执行assign;这样就保证每次工作内存中的值修改后,会马上写入主存中。

保证volatile的重排序规则(下文会有说明)

既然虚拟机对volatile变量做了这么多规定,这样可以保证volatile修饰的变量就是线程安全的吗?看例子:

package test;

import java.util.concurrent.CountDownLatch;

public class Test {

public static volatile int num = 0;

private static CountDownLatch end = new CountDownLatch(20);

public static void addNum() {
num++;
}

public static void main(String[] args) {
for(int i = 0; i < 20; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
for(int i = 0; i < 10000; i++) {
addNum();
}
} finally {
end.countDown();
}
}
}).start();
}

try {
end.await();
System.out.println(num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


说明:20个线程,每个线程对num进行10000次自增操作,如果volatile是线程安全的,那执行完所有线程后应输出200000,但结果每次输出都不同,但都小于200000.

但是虚拟机不是规定对volatile变量的操作会对其他线程立即可见吗?怎么还会输出错误的结果呢?原因是:对num的操作 num++其实是一个复合操作而不是原子操作,也就是说,在执行num++时,会出现”可见性”问题。为了便于理解,可以参照synchronized关键字:

public class SynaTest {

private volatile int num;//volatile变量

public int getNum() {
return num;
}

public void setNum(int num) {
this.num = num;
}

public void add() {
num++;
}
}


等价于

public class SynaTest {

private int num; //普通变量

public synchronized int getNum() {
return num;
}

public synchronized void setNum(int num) {
this.num = num;
}

public void add() {
int tmp = getNum();
tmp = tmp+1;
setNum(tmp);
}
}


至此,关于第一点”对其他线程的可见”说完。

指令重排序

处理器和编译器为提高效率,可能会对程序进行指令重排序,但我们不会意识到这种操作,因为重排序不会影响程序的输出结果,当然,这里不影响输出结果只是在单线程中。那么JMM是如何是volatile修饰的变量不会发生指令重排序呢?

先来说说内存屏障,在JMM中,内存屏障可以分为:

屏障类型指令示例说明
LoadLoadLoad1;LoadLoad;Load2确保Load1数据的装载,之前于Load2及所有后续装载指令的装载
StoreStoreStore1;StoreStore;Store2确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及后续存储指令的存储
LoadStoreLoad1;LoadStore;Store2确保Load1数据装载,之前于Store2及后续的存储指令
StoreLoadStore1;StoreLoad;Load2确保Store1数据对其他处理器变得可见(刷新到内存),之前于Load2及后续装载指令的装载。StoreLoad会使屏障之前的所有内存指令(存储和装载)完成之后,才执行该屏障之后的内存访问指令
在JMM中,关于volatile的重排序规则定义如下:

当第二个操作是volatile写时,不论前一个操作是什么,都不能进行重排序。

当第一个操作是volatile读时,不论后一个操作是什么,都不能进行重排序。

第一个操作是volatile写,后一个操作是volatile读时,不能进行重排序

为了实现上述三点,JMM采用插入内存屏障:

在每个volatile写操作的前面插入一个StoreStore屏障

在每个volatile写操作的后面插入一个StoreLoad屏障

在每个volatile读操作的后面插入一个LoadLoad屏障

在每个volatile读操作的后面插入一个LoadStore屏障

通过这几个内存屏障,JMM就可以保证volatile语义:当写一个volatile变量时,JMM会把该线程对应的工作内存中的值刷新到主存中;档读一个volatile变量时,JMM会把工作内存中对应的变量值设为无效,从主存中获取变量值。

通过上述的描述,可以看出其实volatile并不是” 线程安全”的,如果要保证同步,还需要额外的同步手段,比如通过synchronized关键字或者java.util.concurrent工具,但是volatile在某些情况下是非常适用的,比如只有单一线程对volatile变量进行写操作:

public class VolaTest {

volatile boolean stop = false;

public void shutdown() {//调用该方法后,可以使所有线程的doWork立即停下来
stop = true;
}

public void doWork() {
while(!stop) {
//...
}
}
}


参考:http://ifeve.com/java-memory-model-0/

如果有不对的地方,欢迎大家指正。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: