Java并发编程 (一)—— 内存模型(JMM)

文章目录

  • 1、什么是JMM?
  • 2、计算机内存架构
  • 3、缓存一致性问题
  • 4、指令重排序
  • 5、JMM内存模型
    • 5.1、JMM主内存和工作内存关系
    • 5.2、8 个原子操作和8 个执行规则
      • 5.2.1、8 个原子操作
      • 5.2.2、8 个执行规则
    • 5.3、JMM模型的线程间通信
  • 6、JMM解决的问题
    • 6.1、可见性
      • 6.1.1、定义
      • 6.1.2、可见性问题
      • 6.1.3、如何解决可见性问题
    • 6.2、原子性
      • 6.2.1、 定义
      • 6.2.2、 原子性问题
      • 6.2.3、 如何解决原子性问题
    • 6.3、有序性
      • 6.3.1、定义
      • 6.3.2、有序性问题
      • 6.3.3、如何解决有序性问题
  • 7、Happens-before
    • 7.1、什么是Happens-before
    • 7.2、Happens-before定义
    • 7.3、Happens-before规则

1、什么是JMM?

  JMM 是Java内存模型( Java Memory Model),简称JMM。它本身只是一个抽象的概念,并不真实存在,它描述的是一种规则或规范。通过这组规范,定义了程序中对各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。需要每个JVM 的实现都要遵守这样的规范,有了JMM规范的保障,并发程序运行在不同的虚拟机上时,得到的程序结果才是安全可靠可信赖的。如果没有JMM 内存模型来规范,就可能会出现,经过不同 JVM 翻译之后,运行的结果不相同也不正确的情况。从整体特征上来说,Java内存模型围绕的是在并发场景下如何处理原子性、有序性、可见性的特征来建立的,主要解决的还是并发操作,让程序更适合运行在现代计算机的多任务处理系统中。

2、计算机内存架构

  好了,上面解释那么多,对于JMM的概念和原理,相信还是有人感觉有点懵,懵就对了,因为我刚开始的时候也是这样的。我们都知道要真正深入理解一个问题原理,要从它的起源出发,万物都有起源,那么JMM的起源是什么呢?这里我们从传统的计算机内存架构出发。有的人可能会想,在往前追溯不应该是第一台计算机的产生说起吗?说的没错,奈何本人知识架构体系有限,就不追溯那么远了,废话不多说,直接先上个计算机内存架构图:
Java并发编程 (一)—— 内存模型(JMM)_第1张图片
(1)CPU:去过机房的同学都知道,一般在大型服务器上会配置多个CPU,每个CPU还会有多个核,这就意味着多个CPU或者多个核可以同时(并发)工作。如果使用Java 起了一个多线程的任务,很有可能每个 CPU 都会跑一个线程,那么你的任务在某一刻就是真正并发执行了。
(2)CPU Register:就是 CPU 寄存器。CPU 寄存器是 CPU 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级。
(3)CPU Cache Memory:就是 CPU 高速缓存,相对于寄存器来说,通常也可以成为 L2 二级缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 CPU 还是相差数量级,所以在 CPU 和主存间引入了多级缓存,目的是为了做一下缓冲。
(4)Main Memory:就是主存,主存比 L1、L2 缓存要大很多。
注意:部分高端机器还有 L3 三级缓存。

3、缓存一致性问题

  由于主存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主存中。
使用高速缓存解决了 CPU 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题
Java并发编程 (一)—— 内存模型(JMM)_第2张图片
在多CPU的系统中(或者单CPU多核的系统),每个CPU内核都有自己的高速缓存,它们共享同一主内存(Main Memory)。当多个CPU的运算任务都涉及同一块主内存区域时,CPU 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致。因此需要每个 CPU 访问缓存时遵循一定的协议(即MESI),在读写数据时根据协议进行操作,共同来维护缓存的一致性。那么什么是缓存一致性问题呢?有兴趣的自己可以去看下这篇CPU缓存一致性文章。

4、指令重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:
(1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
(2)指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(
这里的数据依赖性只是在单个处理器中执行的指令序列和单个线程中执行的操作,对于不同处理器和不同线程之间的数据依赖性不被编译器和处理器考虑),处理器可以改变语句对应机器指令的执行顺序;
(3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程下,指令重排可能会导致一些问题。
从 Java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
Java并发编程 (一)—— 内存模型(JMM)_第3张图片
Java 编译器禁止处理器重排序是通过在生成指令序列的适当位置会插入内存屏障(重排序时不能把后面的指令重排序到内存屏障之前的位置)指令来实现的。
指令重排必须满足条件:
(1)as-if-serial语义:对于单线程而言,不管编译器和处理器为了提高并行度做了怎么样的重排序操作,但是执行的结果是不能被改变的。编译器、runtime和处理器都必须遵循 as-if-serial 语义。
(2)遵守数据依赖性:即无论怎么重排序,都不能够改变存在数据依赖性的两个操作的执行顺序。

5、JMM内存模型

  有些人会疑问,有什么上面要解释那么多概念性问题,这些东西跟JMM内存模型有啥关系吗?不要急,慢慢接着往下看。我们知道Java并发编程为了保证线程安全必须满足三要素:可见性、原子性、有序性。再结合我写的三要素文章的定义,进一步深入看问题,可以发现,缓存一致性问题(MESI 优化带来的可见性问题)其实就是可见性问题,CPU优化(在多线程情况下,CPU为了提高执行效率,会进行上下切换从而带来原子性问题)可能会造成原子性问题,指令重排序会造成有序性问题,你看是不是都联系上了。
  出了问题总是要解决的,那有什么办法呢?首先想到简单粗暴的办法,干掉缓存让 CPU 直接与主内存交互就解决了可见性问题,禁止处理器优化和指令重排序就解决了原子性和有序性问题,但这样一夜回到解放前了,显然不可取。所以技术前辈们想到了在物理机器上定义出一套内存模型, 规范内存的读写操作。同一套内存模型规范,不同语言在实现上可能会有些差别。接下来着重讲一下 Java 内存模型实现原理。

5.1、JMM主内存和工作内存关系

  Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。
  Java 内存模型中规定了所有的变量都存储在主内存中,每条线程有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
  这里的工作内存是 JMM 的一个抽象概念,也叫本地内存,其存储了该线程读/写共享变量的副本。就像每个处理器内核拥有私有的高速缓存,JMM 中每个线程拥有私有的本地内存。JMM内存模型如下:
Java并发编程 (一)—— 内存模型(JMM)_第4张图片

5.2、8 个原子操作和8 个执行规则

为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作和8个规则。

5.2.1、8 个原子操作

lock(锁定):作用于主内存,把一个变量标识为一个线程独占状态。
read(读取):作用于主内存,把一个变量的值从主内存传输到线程工作内存中,供之后的 load 操作使用。
load(载入):作用于工作内存,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存,把工作内存中的一个变量传递给执行引擎,虚拟机遇到使用变量值的字节码指令时会执行。
assign(赋值):作用于工作内存,把一个从执行引擎得到的值赋给工作内存的变量,虚拟机遇到给变量赋值的字节码指令时会执行。
store(存储):作用于工作内存,把工作内存中的一个变量传送到主内存中,供之后的 write 操作使用。
write(写入):作用于主内存,把 store 操作从工作内存中得到的变量值存入主内存的变量中。
unlock(解锁):作用于主内存,释放一个处于锁定状态的变量。

5.2.2、8 个执行规则

1、 不允许 read 和 load、store 和write 操作之一单独出现;
2 、不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中;
3、 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中;
4 、一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量。即对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作;
5 、一个变量在同一时刻只允许一条线程对其进行 lock 操作,lock 和 unlock 必须成对出现,但是可以被一个线程多次 lock(锁的可重入);
6 、如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值;
7、 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作;也不允许去 unlock 一个被其他线程锁定的变量;
8、 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)。

5.3、JMM模型的线程间通信

了解完JMM内存和工作内存的关系,我们再结合JMM的8个原子操作和8个执行规则来看线程间是如何完成通信的。
线程A与线程B之间要通信的话,必须要经历下面2个步骤:
(1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
(2)线程B到主内存中去读取线程A之前已更新过的共享变量。
Java并发编程 (一)—— 内存模型(JMM)_第5张图片

6、JMM解决的问题

  我们知道JMM是通过计算器架构引入的规范,但是随之带来的是可见性问题、原子性问题、有序性问题,那么JMM这套规范是如何解决这三个问题呢?

6.1、可见性

6.1.1、定义

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

6.1.2、可见性问题

  我们通过8个原子操作来举个例子。假设有两个线程 A 和 B,其中线程 A 在写入共享变量,线程 B 要读取共享变量,我们想让线程 A 先完成写入,线程 B 再完成读取。此时即便我们是按照 “线程 A 写入 -> 线程 B 读取” 的顺序开始执行的,真实的执行顺序也可能是这样的:storeA -> readB -> writeA -> loadB,这将导致线程 B 读取的是变量的旧值,而非线程 A 修改过的新值。也就是说,线程 A 修改变量的执行先于线程 B 操作了,但这个操作对于线程 B 而言依旧是不可见的。
我们再用代码验证可见性问题:

public class JMMDemo1 {
    public static int a = 0;

    public synchronized static void addA() {
        a++;
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            addA();
            System.out.println(Thread.currentThread().getName() + " 子线程更新后的值为:" + a);
        }).start();
        while (a == 0) {

        }
        System.out.println("主线程执行完毕!具有可见性");
    }
}

  运行后结果如下,可以发现虽然子线程将a的值加了1,但是主线程仍然处于while循环中,说明a++在子线程执行运算后没有马上更新到主内存中,此时主线程去主内存读取到的值是0,因此会一直处于while循环中,最后得出的结论就是a不具有可见性。
Java并发编程 (一)—— 内存模型(JMM)_第6张图片

6.1.3、如何解决可见性问题

那么上面例子中的问题在JMM中怎么解决呢?JMM提供了一个关键字volatile来解决可见性问题。我们这里用volatile关键字修饰a,再看下结果:

public class JMMDemo1 {
    public static volatile int a = 0;

    public static void addA() {
        a++;
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            addA();
            System.out.println(Thread.currentThread().getName() + " 子线程更新后的值为:" + a);
        }).start();
        while (a == 0) {

        }
        System.out.println("主线程执行完毕!具有可见性");
    }
}

  执行后结果如下,发现a值已经被更新为1,主线程执行完毕,具有可见性。
Java并发编程 (一)—— 内存模型(JMM)_第7张图片
那么volatile 又是如何保证可见性呢?这个就要求我们详细的理解volatile,这里不做详细解读,具体我后面会写一volatile篇文章介绍。
除了volatile关键字,还有两个关键字 synchronized 和 final也能保证可见性。因为被 synchronized 包围的代码被线程执行的前提是要 lock,对应的有条规则说到“对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)”。也就是说 synchronized在修改了工作内存中的变量后,解锁前会将工作内存修改的内容刷新到主内存中,确保了共享变量的值是最新的,也就保证了可见性。至于 final 关键字的可见性需要结合其内存语义深入来讲,这里就先简单的概括下:被 final 修饰的字段在构造器中一旦被初始化完成,并且构造器没有把 this 的引用传递出去,那么在其他线程中就能看见 final 字段的值。

6.2、原子性

6.2.1、 定义

一个不可再被分割的颗粒,原子性指的是一个或多个操作要么全部执行成功要么全部执行失败,期间不能被中断,也不存在上下文切换,线程切换会带来原子性的问题。

6.2.2、 原子性问题

原子性定义中说了,线程切换会带来原子性问题,这里我们举个银行ATM存取款的例子:

张三的银行卡有1000余额,由于急需用钱,去银行ATM取出200元;与此同时,之前李四欠了他200元,刚好转入到他的账户。假设张三的操作是线程A,李四的操作是线程B,此时银行卡账户余额就相当于一个共享变量,两人同时读取的余额都是1000元,如果CPU先给线程A分配资源,那么最终的余额应该是1200元,如果CPU先给线程B分配资源,那么最终的余额应该是800元,不管哪个,最后的余额都是错误的,正确的余额应该1000元。

  为了提高CPU的利用率(处理器优化),线程有时候会进行频繁的切换,从上面的例子中可以看出这种切换会带来原子性问题,所以在提高性能的同时往往也会引入新的问题。
  Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write 这 6 个,对基本数据类型的变量的读取和赋值操作是原子性操作(包括byte、short、int、float、boolean、char);对于32位系统的来说,基本数据类型的long和double并非原子性的。也就是说如果存在两条线程同时对long类型或者double类型的数据进行读写是存在相互干扰的,因为对于32位虚拟机来说,每次原子读写是32位的,而long和double则是64位的存储单元。这样会导致一个线程在写时,操作完前32位的原子操作后,轮到B线程读取时,恰好只读取到了后32位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能是“半个变量”的数值,即64位数据被两个线程分成了两次读取。
上面说了volatile关键字修饰变量可以保证可见性,但这并不代表一定是线程安全的,接下来我们举个例子,以a++操作为例,分为三个步骤。
(1)从内存中读取a
(2)对a进行加1操作
(3)将a的值重新写入内存中

public class JMMDemo2 {
    public static volatile int a = 1;
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(
                    new Runnable() {
                        public void run() {
                            try {
                                for (int j = 0; j < 10; j++) {
                                    System.out.println(a++);
                                    Thread.sleep(100);
                                }
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                        }
                    }).start();
        }
    }
}

执行结果如下:
Java并发编程 (一)—— 内存模型(JMM)_第8张图片
上面不仅存在相同的值,最终的结果也不是50,说明a++不是原子性操作。我们接着来分析下,我们把1、2、3步骤理解成get a、add a、put a;在单线程状态下上面的操作没有一点问题,但是在多线程中就会出现各种各样的问题了。因为可能一个线程对a进行了加(add)1操作,还没来得及写(put)入内存,其他的线程就读取(get)了旧值,造成了线程的不安全现象。

6.2.3、 如何解决原子性问题

  其实这在处理器和java编程语言层面,它们都提供了一些有效措施。处理器使用总线锁定和缓存锁定这两个机制来保证复杂内存操作的原子性,Java 提供了锁和循环 CAS 的方式。
  如果应用场景需要一个更大范围的原子性保证,Java 内存模型还提供了 lock 和 unlock 操作来满足这种需求。尽管 JVM 并没有把 lock 和 unlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式地使用这两个操作。这两个字节码指令反映到 Java 代码中就是同步块 — synchronized 关键字,因此在 synchronized 块之间的操作也具备原子性。而除了 synchronized 关键字这种 Java 语言层面的锁,juc 并发包中的 java.util.concurrent.locks.Lock 接口也提供了一些类库层面的锁,比如 ReentrantLock。
  另外,随着硬件指令集的发展,在 JDK 5 之后,Java 类库中开始使用基于 cmpxchg 指令的 CAS 操作(又来一个重点),该操作由 sun.misc.Unsafe 类里面的 compareAndSwapInt() 和 compareAndSwapLong() 等几个方法包装提供。不过在 JDK 9 之前 Unsafe 类是不开放给用户使用的,只有 Java 类库可以使用,譬如 juc 包里面的整数原子类,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作来实现。使用这种 CAS 措施的代码也常被称为无锁编程(Lock-Free)。
最后总结下解决原子性的方法有哪些:
(1)通过 synchronized 关键字和lock锁
(2)通过 CAS(java.concurrent.Atomic.* 包中所有类的一切操作)
知道了决绝原子性问题的方法后,我们用CAS方法来解决上面那个a++问题,修改后的代码如下:

    public static volatile AtomicInteger a = new AtomicInteger();
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(
                    new Runnable() {
                        public void run() {
                            try {
                                for (int j = 0; j < 10; j++) {
                                    System.out.println(a.incrementAndGet());
                                    Thread.sleep(100);
                                }
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }

                        }
                    }).start();
        }
    }
}

执行结果如下:
Java并发编程 (一)—— 内存模型(JMM)_第9张图片
上面的结果值是50,也没有出现重复的数值,说明此时a++已经保证了原子性。

6.3、有序性

6.3.1、定义

程序执行的顺序按照代码的先后顺序执行,因为处理器可能会对指令进行重排序
JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)

6.3.2、有序性问题

上面说了指令重排序会造成有序性问题,我们来举个典型的例子:

public class JMMDemo3 {
    int a =0;
    boolean flag = false;
    //线程A
    public void writer() {
        a= 1; //1
        flag = true; //2
    }
    //线程B
    public void reader(){
        if (flag){// 3
            int i =a*a;// 4
            System.out.println(i);
        }
    }
}

假设有两个线程A和B,线程A执行writer(),线程B执行reader(),线程B在执行操作4时,是不一定能看到线程A在操作1对共享变量a的写入,这是由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。
Java并发编程 (一)—— 内存模型(JMM)_第10张图片
按照上面这种执行顺序线程B肯定读不到线程A设置的a值,在这里多线程的语义就已经被重排序破坏了。
操作3 和操作4 之间也可以重排序,这里就不阐述了。但是他们之间存在一个控制依赖的关系,因为只有操作3 成立操作4 才会执行。当代码中存在控制依赖性时,会影响指令序列的执行的并行度,所以编译器和处理器会采用猜测执行来克服控制依赖对并行度的影响。假如操作3 和操作4重排序了,操作4 先执行,则先会把计算结果临时保存到重排序缓冲中,当操作3 为真时才会将计算结果写入变量i中
通过上面的分析,重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义。

6.3.3、如何解决有序性问题

  那 JMM 又是怎么来处理多线程下的指令重排呢?想想 CPU 是怎么处理的,内存屏障,JMM 直接搬过来,嗯,我也使用一些特殊的指令来达到某些指令的顺序执行。没错,还是volatile 关键字,通过关键字修饰来禁止指令重排。
  另外,JMM 为了保证有序,还内置了一套先行发生规则(happens-before)两个操作间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行。happens-before 仅仅要求前一个操作对后一个操作可见,和一般意义上时间的先后是不一样的,达到逻辑上的顺序执行即可。happens-before我会在第七点中讲到。

7、Happens-before

7.1、什么是Happens-before

  一方面,程序员需要JMM提供⼀个强的内存模型来编写代码;另一方面,编译器和处理器希望JMM对它们的束缚越少越好,这样它们就可以最可能多的做优化来提高性能,希望的是一个弱的内存模型。
  JMM考虑了这两种需求,并且找到了平衡点,对编译器和处理器来说,只要不改变程序的执行结果(单线程程序和正确同步了的多线程程序),编译器和处理器怎么优化都行。
  而对于程序员,JMM提供了happens-before规则(JSR-133规范),满足了程序员的需求——简单易懂,并且提供了组够强的内存可见性保证。换言之,程序员只要遵循happens-before规则,那他写的程序就能保证在JMM中具有强的内存可见性。

7.2、Happens-before定义

(1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
(2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

7.3、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。
(5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
(6)Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
(7)程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
(8)对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
举个例子:

public class HappensBeforeDemo1 {
    public static void main(String[] args) {
        int a = 1; //A
        int b = 2; //B
        int c = a * 2; //C
    }
}

上面例子满足规则中的第一条“程序顺序规则”。只有一个main主线程,happens-before存在三个关系:
(1)A happens-before B;
(2)B happens-before C;
(3)A happens-before C。
A happens-before B,定义1要求A执行结果对B可见,并且A操作的执行顺序在B操作之前,但与此同时利用定义中的第二条,A,B操作彼此不存在数据依赖性,两个操作的执行顺序对最终结果都不会产生影响,在不改变最终结果的前提下,允许A,B两个操作重排序,即happens-before关系并不代表了最终的执行顺序。
所以,重排序有两类,JMM对这两类重排序有不同的策略:
(1)会改变程序执⾏结果的重排序,⽐如 A -> C,JMM要求编译器和处理器都禁⽌这种重排序。
(2)不会改变程序执⾏结果的重排序,⽐如 A -> B,JMM对编译器和处理器不做要求,允许这种重排序。
到这里,发现happens-before关系本质上和as-if-serial语义是⼀回事。
as-if-serial语义保证单线程内重排序后的执⾏结果和程序代码本身应有的结果是⼀致的,happens-before关系保证正确同步的多线程程序的执⾏结果不被重排序改变。
总之,如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可⻅的,不管它们在不在⼀个线程。

你可能感兴趣的:(JMM,java线程,java)