并发编程-有序性(一)

177 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第22天,点击查看活动详情

有序性

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; 
j = ...; 

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是

i = ...; 
j = ...;

也可以是

j = ...;
i = ...; 

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU 执行指令的原理来理解一下吧。

原理之指令级并行

名词

Clock Cycle Time

主频的概念大家接触的比较多,而 CPU 的 Clock Cycle Time(时钟周期时间),等于主频的倒数,意思是 CPU 能 够识别的最小时间单位,比如说 4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns,作为对比,我们墙上挂钟的 Cycle Time 是 1s

例如,运行一条加法指令一般需要一个时钟周期时间

CPI

有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction)指令平均时钟周期数

IPC

IPC(Instruction Per Clock Cycle) 即 CPI 的倒数,表示每个时钟周期能够运行的指令数

CPU 执行时间

程序的 CPU 执行时间,即我们前面提到的 user + system 时间,可以用下面的公式来表示

程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time

指令重排序优化

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令 还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段。

术语参考

  • instruction fetch (IF)

  • instruction decode (ID)

  • execute (EX)

  • memory access (MEM)

  • register write back (WB)

在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序组合来实现指令级并行,这一技术在 80's 中 叶到 90's 中叶占据了计算架构的重要地位。

提示

分阶段,分工是提升效率的关键!

指令重排的前提是,重排指令不能影响结果,例如

// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );
// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2

参考

Scoreboarding and the Tomasulo algorithm (which is similar to scoreboarding but makes use of register renaming )are two of the most common techniques for implementing out-of-order execution and instruction-level parallelism.

支持流水线的处理器

现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理 器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一 条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了 指令地吞吐率。

提示

奔腾四(Pentium 4)支持高达 35 级流水线,但由于功耗太高被废弃

SuperScalar 处理器

大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单 元等,这样可以把多条指令也可以做到并行获取、译码等,CPU 可以在一个时钟周期内,执行多于一条指令,IPC > 1。

诡异的结果

int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
    if(ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}
// 线程2 执行此方法
public void actor2(I_Result r) { 
    num = 2;
    ready = true; 
}

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?

有同学这么分析

情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1

情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

但我告诉你,结果还有可能是 0 😁😁😁,信不信吧!

这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2

相信很多人已经晕了 😵😵😵

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:

借助 java 并发压测工具 jcstress wiki.openjdk.java.net/display/Cod…

mvn archetype:generate -DinteractiveMode=false 
-DarchetypeGroupId=org.openjdk.jcstress 
-DarchetypeArtifactId=jcstress-java-test-archetype
-DarchetypeVersion=0.5 
-DgroupId=cn.itcast 
-DartifactId=ordering 
-Dversion=1.0 

创建 maven 项目,提供如下测试类

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
    int num = 0;
    boolean ready = false;
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

执行

mvn clean install 
java -jar target/jcstress.jar 

会输出我们感兴趣的结果,摘录其中一次结果:

*** INTERESTING tests 
 Some interesting behaviors observed. This is for the plain curiosity. 
 
 2 matching test results. 
  [OK] test.ConcurrencyTest 
  (JVM args: [-XX:-TieredCompilation]) 
    Observed state  Occurrences   Expectation Interpretation 
    0         1,729       ACCEPTABLE_INTERESTING !!!! 
  1         42,617,915    ACCEPTABLE ok 
  4         5,146,627     ACCEPTABLE ok 
 
  [OK] test.ConcurrencyTest 
  (JVM args: []) 
  Observed state  Occurrences   Expectation Interpretation 
  0         1,652       ACCEPTABLE_INTERESTING !!!! 
  1         46,460,657    ACCEPTABLE ok 
  4         4,571,072     ACCEPTABLE ok 

可以看到,出现结果为 0 的情况有 638 次,虽然次数相对很少,但毕竟是出现了。