Java内存模型(二)

时间:2015-02-01 20:24:31   收藏:0   阅读:296

volatile型变量的特殊规则

volatile是Java虚拟机提供的最轻量级的同步机制,当一个变量被定义成volatile后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程改变了这个变量的值后,新值对于其他线程来说是可以立即得知的;第二个语义是禁止指令重排序,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

开发人员经常误解的一个描述:“volatile变量对于所有线程是立即可见的,对volatile变量的写操作都能立刻反应到其他线程之中,换句话说,volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是安全的”。这句话的论据部分并没有错,但是其论据不能得出“基于volatile变量的运算在并发下是安全的”这个结论。volatile型变量在各个线程的工作内存中不存在一致性问题,但是Java里面的运算并非原子运算,导致volatile变量的运算在并发下一样是不安全的。 象如下代码:

1
2
volatile int race =0 ;
race++

我们知道race++这个操作并不是原子运算,它会被编译成多条字节码指令,故此多线程计算时会出现结果不符合预期的情况。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然需要通过加锁来保证原子性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Map configOptions;
char[] configText;
volatile boolean initialized = false;
//假设以下代码在线程A中执行
//模拟读取配置信息,当读取完成后
//将initialized设置为true 来通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized = true;
 
//假设以下代码在线程B中执行
while(!initialized){
 sleep();
}
doSomethingWithConfig();

上述代码是一段伪代码,其中描述的场景十分常见,只是我们在处理配置文件时一般不会出现并发而已。如果定义initialized变量时没有使用volatile修饰,就可能由于指令重排序的优化,导致位于线程A中最后一句的代码”initialized=true”被提前执行,这样在线程B中使用配置信息的代码可能出现错误,而volatile关键字则可避免此类情况的发生。

再看看Java内存模型中对volatile变量定义的特殊规则,假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read,load,use,assign,store和write操作时需要满足以下规则:

原子性、可见性和有序性

先行发生原则(Happen-before)

先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A所产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值,发送了消息,调用了方法等。

Java内存模型中“天然的”先行发生关系:

其中程序次序规则,管程锁定规则,volatile变量规则,传递性规则经常用来推断先行发生关系。

需要注意的是,先行发生关系和时间上的先后顺序基本没有太大的关系。

时间上的先后顺序不能得出先行发生关系,示例代码如下所示:

1
2
3
4
5
6
7
private int value=0;
public void setValue(int value){
  this.value=value;
}
public int getValue(){
return value;
}

假设存在线程A和线程B,线程A先(时间上的先后)调用了”setValue(1)”,然后线程B调用了同一个对象的”getValue()”,那么线程B收到的返回值是不确定的,由于工作内存和主内存同步存在延迟,也由于可能存在重排序现象。 虽然时间上线程A的setValue()操作先于线程B的getValue()操作,但是并不能推断出线程A的setValue()操作先行发生于线程B的getValue()操作,如果有这种先行发生关系,那么可以推断出线程B的getValue()操作获得的值。

如果我们给getValue()方法和setValue()方法添加synchronized关键字,就能利用管程锁定规则推断出线程A的setValue操作先行发生于线程B的getValue操作,或者我们也可以将value定义为volatile变量,也能利用volatile变量规则推断出先行发生关系。

先行发生关系也不能推断出时间上的先后执行顺序,示例代码如下所示:

1
2
int i=1;
int j=2;

根据程序次序规则,我们可以推断出”int i=1”的操作先行发生于”int j=2”的操作,但是”int j=2”的代码完全有可能先被处理器执行(时间上的先后),这就是重排序,虚拟机规范是允许这种特性存在的,虚拟机可利用这种特性提高性能。

重排序

在同一个线程操作是顺序执行的(其实是按控制流执行),但是在某个线程看别的线程里的操作,则是乱序执行的。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class PossibleReording {
	private static int x = 0, y = 0;
	private static int a = 0, b = 0;
 
	public static void main(String[] args) 
	        throws InterruptedException {
		Thread one = new Thread() {
			@Override
			public void run() {
				a = 1;
				x = b;
			}
		};
		Thread other = new Thread() {
			@Override
			public void run() {
				b = 1;
				y = a;
			}
		};
		one.start();
		other.start();
		one.join();
		other.join();
		System.out.println("(" + x + "," + y + ")");
 
	}
 
}

这段代码的执行结果是什么呢?我们先不考虑工作内存和主内存的同步问题,假设一种执行顺序,one线程先执行完,然后other线程再执行,那么执行的操作序列会是a=1, x=b, b=1, y=a; 此时的输出结果将是(0,1) 假设另一种执行顺序,other线程先执行,one线程后执行,那么执行的操作序列是b=1,y=a,a=1,x=b,此时的输出结果是(1,0),如果one线程和other线程交替执行,a=1,b=1,y=a,x=b,此时的输错结果是(1,1)。

但是让意外的是竟然还会存在这样一种输出结果(0,0),这种结果很难想象。这便是重排序导致的现象。一种可能的重排序后的执行顺序如下图所示:

技术分享

线程A执行时本来应该先执行a=1,后执行x=b的,但是由于重排序的原因x=b先执行,a=1后执行。

这一节说的的先后是指时间上的先后,根据内存模型的程序次序规则,线程A里a=1还是先行发生于x=b的。

参考资料

《深入理解Java虚拟机》 第12章 Java内存模型与线程

《Java多线程设计模式》 附录B Java的内存模型。

转载 http://www.cloudchou.com/softdesign/post-637.html

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!