澳门金沙vip 11

【澳门金沙vip】Java并发编程(十四)Java内存模型

顺序一致性模型

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:

  1. 一个线程中的所有操作必须按照程序的顺序来执行。
  2. (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

顺序一致性内存模型为程序员提供的视图如下: 

澳门金沙vip 1

在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程。同时,每一个线程必须按程序的顺序来执行内存读/写操作。从上图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化。

顺序一致性内存模型中的每个操作必须立即对任意线程可见,但是在JMM中就没有这个保证。未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,且还没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本还没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其它线程看到的操作执行顺序将不一致。

数据竞争与顺序一致性

当程序未正确同步时,就会存在数据竞争。数据竞争指的是:在一个线程中写一个变量,在另一个线程读同一个变量,而且写和读没有通过同步来排序。
当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。
JMM对正确同步的多线程程序的内存一致性做了如下保证:
如果程序是正确同步的,程序的执行将具有顺序一致性(sequentially
consistent),即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。这里的同步是指广义上的同步,包括对常用同步原语(synchronized,volatile和final)的正确使用。

3、顺序一致性

未同步程序的顺序一致性

JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为未同步程序在顺序一致性模型中执行时,整体上是无序的,其执行结果无法预知。保证未同步程序在两个模型中的执行结果一致毫无意义。 
和顺序一致性模型一样,未同步程序在JMM中的执行时,整体上也是无序的,其执行结果也无法预知。 
同时,未同步程序在这两个模型中的执行特性有下面几个差异:

  1. 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。
  2. 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。
  3. JMM不保证对64位的long型和double型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。

对于第三个差异:在一些32位的处理器上,如果要求对64位数据的读/写操作具有原子性,会有比较大的开销。为了照顾这种处理器,java语言规范鼓励但不强求JVM对64位的long型变量和double型变量的读/写具有原子性。当JVM在这种处理器上运行时,会把一个64位long/
double型变量的读/写操作拆分为两个32位的读/写操作来执行。这两个32位的读/写操作可能会被分配到不同的总线事务中执行,此时对这个64位变量的读/写将不具有原子性。 
当单个内存操作不具有原子性,将可能会产生意想不到后果。请看下面示意图: 
澳门金沙vip 2

如上图所示,假设处理器A写一个long型变量,同时处理器B要读这个long型变量。处理器A中64位的写操作被拆分为两个32位的写操作,且这两个32位的写操作被分配到不同的写事务中执行。同时处理器B中64位的读操作被拆分为两个32位的读操作,且这两个32位的读操作被分配到同一个的读事务中执行。当处理器A和B按上图的时序来执行时,处理器B将看到仅仅被处理器A“写了一半“的无效值。

 

1.共享内存和消息传递

线程之间的通信机制有两种:共享内存和消息传递;在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。工程师必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对工程师完全透明。

4.3 volatile内存语义的实现

为了实现volatile内存语义,JMM会分别限制编译器重排序和处理器重排序。下面是JMM针对编译器制定的volatile重排序规则表:

 

是否能重排序

第二个操作

第一个操作

普通读/写

volatile读

volatile写

普通读/写

 

 

NO

volatile读

NO

NO

NO

volatile写

 

NO

NO

举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。

从上表我们可以看出:

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

转自

同步程序的顺序一致性

我们接下来看看正确同步的程序如何具有顺序一致性。

澳门金沙vip 3澳门金沙vip 4

class SynchronizedExample {
int a = 0;
boolean flag = false;

public synchronized void writer() {
    a = 1;
    flag = true;
}

public synchronized void reader() {
    if (flag) {
        int i = a;
        ……
    }
}
}

View Code

上面示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法。这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。下面是该程序在两个内存模型中的执行时序对比图:

澳门金沙vip 5

在顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序(但JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。JMM会在退出监视器和进入监视器这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图。虽然线程A在临界区内做了重排序,但由于监视器的互斥执行的特性,这里的线程B根本无法“观察”到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。 
从这里我们可以看到JMM在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能的为编译器和处理器的优化打开方便之门。

JMM的设计意图

在设计JMM需要考虑两个关键因素:

  1. 工程师对内存模型的使用,希望内存模型易于理解和编程,工程师希望基于一个强内存模型来编写代码。
  2. 编译器和处理器对内存的实现,希望内存模型对他们的束缚越少越好,编译器和处理器希望实现一个弱内存模型。

这两个因素是互相矛盾的,所以JSR-133专家组设计时需要考虑到一个好的平衡点:一方面为工程师提供足够强的内存可见性,另一方面要对编译器和处理器的限制要尽量松些。

我们来举了例子:

int a=10;   //A
int b=20;   //B
int c=a*b;  //C

上面是一个简单的乘法运算,并存在3个happens-before关系:

  1. A happens-before B
  2. B happens-before C
  3. A happens-before C

这三个happens-before关系中,2和3是必须的,但1是不必要的。因此,JMM把happens-before要求禁止的重排序分为两类:

  1. 会改变程序执行结果的重排序。
  2. 不会改变程序执行结果的重排序。

JMM对这两种不同性质的重排序,采取了不同的策略:

  1. 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
  2. 对于不会改变程序执行结果的重排序,JMM要求编译器和处理器不做要求,可以允许这种重排序。

1.3 synchronized与volatile

一个线程执行互斥代码过程如下:

  1. 获得同步锁;
  2. 清空工作内存;
  3. 从主内存拷贝对象副本到工作内存;
  4.  执行代码(计算或者输出等);
  5. 刷新主内存数据;
  6. 释放同步锁。

所以,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。

volatile是第二种Java多线程同步的手段,根据JLS的说法,一个变量可以被volatile修饰,在这种情况下内存模型确保所有线程可以看到一致的变量值

class Test {    
    static volatile int i = 0, j = 0;    
    static void one() {    
        i++;    
        j++;    
    }    
    static void two() {    
        System.out.println("i=" + i + " j=" + j);    
    }    
}  

 

加上volatile可以将共享变量i和j的改变直接响应到主内存中,这样保证了i和j的值可以保持一致,然而我们不能保证执行two方法的线程是在i和j执行到什么程度获取到的,所以volatile可以保证内存可见性,不能保证并发有序性

 

如果没有volatile,则代码执行过程如下:

 

  1. 将变量i从主内存拷贝到工作内存;

  2. 刷新主内存数据;

  3. 改变i的值;

  4. 将变量j从主内存拷贝到工作内存;

  5. 刷新主内存数据;

  6. 改变j的值;

数据竞争与顺序一致性

当程序未正确同步时,就会存在数据竞争。数据竞争指的是:在一个线程中写一个变量,在另一个线程读同一个变量,而且写和读没有通过同步来排序。 
当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。 
JMM对正确同步的多线程程序的内存一致性做了如下保证: 
如果程序是正确同步的,程序的执行将具有顺序一致性(sequentially
consistent),即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。这里的同步是指广义上的同步,包括对常用同步原语(synchronized,volatile和final)的正确使用。

5.顺序一致性

顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型为参考。

Java的并发采用的是共享内存模型(而非消息传递模型),线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。多个线程之间是不能直接传递数据交互的,它们之间的交互只能通过共享变量来实现

5.顺序一致性

顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型为参考。

happens-before的定义与规则

JSR-133使用happens-before的概念来指定两个操作之间的执行顺序,由于这两个操作可以在一个线程内,也可以在不同的线程之间。因此,JMM可以通过happens-before关系向工程师提供跨线程的内存可见性保证。

happens-before规则如下:

  1. 程序顺序规则:一个线程中的每个操作,happens- before
    于该线程中的任意后续操作。
  2. 监视器锁规则:对一个监视器锁的解锁,happens- before
    于随后对这个监视器锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens- before
    于任意后续对这个volatile域的读。
  4. 传递性:如果A happens- before B,且B happens- before C,那么A
    happens- before
    C。

3.2 顺序一致性内存模型

顺序一致性内存模型有两大特性:

  • 一个线程中的所有操作必须按照程序的顺序来执行。
  • (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

 

顺序一致性内存模型为程序员提供的视图如下。在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程。同时,每一个线程必须按程序的顺序来执行内存读/写操作。在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化。

澳门金沙vip 6

为了更好的理解,下面我们通过两个示意图来对顺序一致性模型的特性做进一步的说明。

假设有两个线程A和B并发执行。其中A线程有三个操作,它们在程序中的顺序是:A1->A2->A3。B线程也有三个操作,它们在程序中的顺序是:B1->B2->B3。

假设这两个线程使用监视器来正确同步:A线程的三个操作执行后释放监视器,随后B线程获取同一个监视器。那么程序在顺序一致性模型中的执行效果将如下图所示:

澳门金沙vip 7

假设这两个线程没有做同步,下面是这个未同步程序在顺序一致性模型中的执行示意图:

澳门金沙vip 8

 

未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程A和B看到的执行顺序都是:B1->A1->A2->B2->A3->B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。

但是,在JMM中就没有这个保证。未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,且还没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本还没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其它线程看到的操作执行顺序将不一致。

 3.3 同步程序的执行特性

【例】

class SynchronizedExample {  
  int a = 0;  
  boolean flag = false;  

  public synchronized void writer() {  
    a = 1;  
    flag = true;  
  }  

  public synchronized void reader() {  
    if (flag) {  
        int i = a;  
        ……  
    }  
  }  
} 

 

 澳门金沙vip 9

 

在顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序。

1.共享内存和消息传递

  线程之间的通信机制有两种:共享内存和消息传递;在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。工程师必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对工程师完全透明。

 

2.Java内存模型的抽象
  在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量,方法定义参数和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:

澳门金沙vip 10

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 线程B到主内存中去读取线程A之前已更新过的共享变量。

 

3.从源代码到指令序列的重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 

从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

澳门金沙vip 11

上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。 
JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

 

4.happens-before简介
happens-before是JMM最核心的概念,对于Java工程师来说,理解happens-before是理解JMM的关键。

JMM的设计意图

在设计JMM需要考虑两个关键因素:

  1. 工程师对内存模型的使用,希望内存模型易于理解和编程,工程师希望基于一个强内存模型来编写代码。
  2. 编译器和处理器对内存的实现,希望内存模型对他们的束缚越少越好,编译器和处理器希望实现一个弱内存模型。

这两个因素是互相矛盾的,所以JSR-133专家组设计时需要考虑到一个好的平衡点:一方面为工程师提供足够强的内存可见性,另一方面要对编译器和处理器的限制要尽量松些。

我们来举了例子:

澳门金沙vip 12澳门金沙vip 13

int a=10;   //A
int b=20;   //B
int c=a*b;  //C

上面是一个简单的乘法运算,并存在3个happens-before关系:
1.  A happens-before B
2.  B happens-before C
3.  A happens-before C

这三个happens-before关系中,2和3是必须的,但1是不必要的。因此,JMM把happens-before要求禁止的重排序分为两类:
1.会改变程序执行结果的重排序。
2.不会改变程序执行结果的重排序。


JMM对这两种不同性质的重排序,采取了不同的策略:
1.对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
2.对于不会改变程序执行结果的重排序,JMM要求编译器和处理器不做要求,可以允许这种重排序。

View Code

happens-before的定义与规则

JSR-133使用happens-before的概念来指定两个操作之间的执行顺序,由于这两个操作可以在一个线程内,也可以在不同的线程之间。因此,JMM可以通过happens-before关系向工程师提供跨线程的内存可见性保证。

happens-before规则如下: 
1.
程序顺序规则:一个线程中的每个操作,happens- before
于该线程中的任意后续操作。 
2.
监视器锁规则:对一个监视器锁的解锁,happens- before
于随后对这个监视器锁的加锁。 
3.
volatile变量规则:对一个volatile域的写,happens- before
于任意后续对这个volatile域的读。 
4.
传递性:如果A happens- before B,且B happens- before C,那么A happens-
before C。

 

相关文章
Java并发编程(一)线程定义、状态和属性
Java并发编程(二)同步
Java并发编程(三)volatile域

4.1 volatile写-读建立的happens before关系

从JSR-133开始,volatile变量的写-读可以实现线程之间的通信。

从内存语义的角度来说,volatile与监视器锁有相同的效果:volatile写和监视器的释放有相同的内存语义;volatile读与监视器的获取有相同的内存语义

class VolatileExample {  
    int a = 0;  
    volatile boolean flag = false;  

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

    public void reader() {  
        if (flag) {                //3  
            int i =  a;           //4  
            ……  
        }  
    }  
}  

 

假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens
before规则,这个过程建立的happens before 关系可以分为两类:

  1. 根据程序次序规则,1 happens before 2; 3 happens before 4。
  2. 根据volatile规则,2 happens before 3。
  3. 根据happens before 的传递性规则,1 happens before 4。

 澳门金沙vip 14

 

 上图中,每一个箭头链接的两个节点,代表了一个happens before
关系。黑色箭头表示程序顺序规则;橙色箭头表示volatile规则;蓝色箭头表示组合这些规则后提供的happens
before保证。

这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。

顺序一致性模型

顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:

  1. 一个线程中的所有操作必须按照程序的顺序来执行。
  2. (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

顺序一致性内存模型为程序员提供的视图如下:

这里写图片描述

在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程。同时,每一个线程必须按程序的顺序来执行内存读/写操作。从上图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化。

顺序一致性内存模型中的每个操作必须立即对任意线程可见,但是在JMM中就没有这个保证。未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,且还没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本还没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其它线程看到的操作执行顺序将不一致。

2.4 重排序对多线程的影响

现在让我们来看看,重排序是否会改变多线程程序的执行结果。【例】:

class ReorderExample {  
    int a = 0;  
    boolean flag = false;  

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

    Public void reader() {  
        if (flag) {                //3  
            int i =  a * a;        //4  
            ……  
        }  
    }  
}  

flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入?

答案是:不一定能看到。

 由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系(?),编译器和处理器也可以对这两个操作重排序。让我们先来看看,当操作1和操作2重排序时,可能会产生什么效果?请看下面的程序执行时序图:

澳门金沙vip 15

如上图所示,操作1和操作2做了重排序。程序执行时,线程A首先写标记变量flag,随后线程B读这个变量。由于条件判断为真,线程B将读取变量a。此时,变量a还根本没有被线程A写入,在这里多线程程序的语义被重排序破坏了!

 下面再让我们看看,当操作3和操作4重排序时会产生什么效果(借助这个重排序,可以顺便说明控制依赖性)。下面是操作3和操作4重排序后,程序的执行时序图:

澳门金沙vip 16

在程序中,操作3和操作4存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder
buffer
ROB)的硬件缓存中。当接下来操作3的条件判断为真时,就把该计算结果写入变量i中。

从图中我们可以看出,猜测执行实质上对操作3和4做了重排序。重排序在这里破坏了多线程程序的语义!

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果