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

来源:互联网 发布:手机淘宝新品上架文字 编辑:程序博客网 时间:2024/06/16 02:20

假设有两个线程分别调用同一个test对象的writer()和reader()。请问,b的值是什么?

(a) 1
(b) 2
(c) 1 or 2

 public class test{    private boolean flag = false;    private int a = 0;    public void writer(){            a = 1;            flag = True;    }    public void reader(){        if (flag){            b = a + 1        }    }}

这里主要涉及的是处理器重排序问题。当前处理器为了加速指令执行,会将部分指令重排序之后执行。

数据依赖

数据依赖是一个简单的概念,就是判断前后两行代码在数据上有否有依赖关系。例如:

num1 = 1                // (a)num2 = 2                // (b)result = num1 + num2    // (c)

显然,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)

对于上述三条语句 a, b, c执行,单线程顺序执行的情况。

a happen before b       b happen before c。

根据传递性可以得出:

a happen before c

c指令要用到的 num1 和 num2 显然是依赖 a 和 b 的,典型的store-load。所以c指令必须等到 a 和 b 执行完才能执行。然而 a 和 b 并没有数据依赖,于是 JVM 允许处理器对 a 和 b 进行重排序。

a -> b -> c = 3b -> a -> c = 3

那么happen before到底是什么?我的理解是happen before是JVM对底层内存控制抽象出一层概念。我们可以根据代码顺序来判断happen before的关系,而JVM底层会根据实际情况执行不同的 action (例如添加内存屏障,处理器屏障,阻止重排序又或者是不做任何额外操作,允许处理器冲排序)。通过这一层使得内存控制对程序员透明,程序员也不需要考虑代码实际执行情况,JVM会保证单线程执行成功,as-if-serial。

既然JVM已经透明了内存控制,那为什么要搞清楚这点,那就是JVM只保证单线程执行成功,而多线程环境下,就会出各种各样的问题。

答案

下面就用上述讲的分析一下最初的题目。

A线程执行:

    public void writer(){            a = 1;              // (1)            flag = True;        // (2)    }

B线程执行:

    public void reader(){        if (flag){              // (3)            b = a + 1           // (4)        }    }

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

原创粉丝点击