并发中的陷阱-处理器重排序

假设有两个线程分别调用同一个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 保证数据一致性,但是在多线程环境下,还是会有很多诡异的问题发生,这个时候就要考虑处理器,编译器重排序。

你可能感兴趣的:(并发中的陷阱-处理器重排序)