假设有两个线程分别调用同一个test对象的writer()和reader()。请问,b的值是什么?
(a) 1
(b) 2
(c) 1 or 2
publicclasstest{privateboolean flag =false;privateinta =0;publicvoidwriter(){ a =1; flag = True; }publicvoidreader(){if(flag){ b = a +1} }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
这里主要涉及的是处理器重排序问题。当前处理器为了加速指令执行,会将部分指令重排序之后执行。
数据依赖
数据依赖是一个简单的概念,就是判断前后两行代码在数据上有否有依赖关系。例如:
num1 =1 // (a)num2 =2 // (b)result= num1 + num2 // (c)
1
2
3
显然,c 语句用到的 num1 和 num2 依赖 a 和 b。
数据依赖分三种:
1 store - load
2 load - store
3 store - store
如何判断是否有依赖,很简单,只用判断两个语句之间是否用到同一个变量,是否是写操作。
Happen before
JVM定义了一个概念叫做 happen before,意思是前一条执行的结果要对后一条执行可见。简单来说前一条执行完,才能执行后一条。但实际上为了提高处理速度,JVM弱化了这个概念,在有数据依赖的情况下,前一条执行完,才能执行后一条。
看下面的例子:
num1 =1 // (a)num2 =2 // (b)result= num1 + num2 // (c)
1
2
3
对于上述三条语句 a, b, c执行,单线程顺序执行的情况。
ahappenbeforeb b happenbeforec。
1
2
3
根据传递性可以得出:
ahappenbeforec
1
c指令要用到的 num1 和 num2 显然是依赖 a 和 b 的,典型的store-load。所以c指令必须等到 a 和 b 执行完才能执行。然而 a 和 b 并没有数据依赖,于是 JVM 允许处理器对 a 和 b 进行重排序。
a-> b -> c =3b-> a -> c =3
1
2
那么happen before到底是什么?我的理解是happen before是JVM对底层内存控制抽象出一层概念。我们可以根据代码顺序来判断happen before的关系,而JVM底层会根据实际情况执行不同的 action (例如添加内存屏障,处理器屏障,阻止重排序又或者是不做任何额外操作,允许处理器冲排序)。通过这一层使得内存控制对程序员透明,程序员也不需要考虑代码实际执行情况,JVM会保证单线程执行成功,as-if-serial。
既然JVM已经透明了内存控制,那为什么要搞清楚这点,那就是JVM只保证单线程执行成功,而多线程环境下,就会出各种各样的问题。
答案
下面就用上述讲的分析一下最初的题目。
A线程执行:
publicvoidwriter(){ a =1;// (1)flag = True;// (2)}
1
2
3
4
B线程执行:
publicvoidreader(){if(flag){// (3)b = a +1// (4)} }
1
2
3
4
5
1.先考虑大多数人考虑的情况:
指令顺序:(1)-> (2) -> (3) -> (4),b = 1 +1 = 2
2.意想不到的情况
对于A线程来说,语句 (1)和(2)并不存在任何数据依赖问题。因此处理器可以对其进行重排序,也就是指令 (2)可能会先于指令(1)执行。
那么当指令按照(2)-> (3) -> (4) -> (1) 顺序,b = 0 +1 = 1
3.还有一种情况
对于B线程,处理器可能会提前处理 (4),将结果放到 ROB中,如果控制语句(3)为真,就将结果从ROB取出来直接使用,这是一种优化技术,预测。
所以指令执行顺序可能是 (4) -> x -> x ->x
看来4条语句都有可能最先被执行。
总结一下,在多处理器环境中,由于每个处理器都有自己的读写缓存区,所以会使部分数据不一致。JMM会有一系列 action 保证数据一致性,但是在多线程环境下,还是会有很多诡异的问题发生,这个时候就要考虑处理器,编译器重排序。