本文已收录到 AndroidFamily,技能和职场问题,请关注大众号 [彭旭锐] 发问。

前言

大家好,我是小彭。

在上一篇文章里,咱们聊到了 CPU 的缓存共同性问题,分为纵向的 Cache 与内存的共同性问题以及横向的多个中心 Cache 的共同性问题。咱们也评论了 MESI 协议经过写传达和业务串行化完成缓存共同性。

不知道你是不是跟我相同,在学习 MESI 协议的时候,自然地产生了一个疑问:在不考虑写缓冲区和失效行列的影响下,在硬件层面现已完成了缓存共同性,那么在 Java 言语层面为什么还需求界说 volatile 关键字呢?是多此一举吗?今日咱们将环绕这些问题打开。


学习路线图:

已经有 MESI 协议,为什么还需要 volatile 关键字?


1. 回顾 MESI 缓存共同性协议

因为 CPU 和内存的速度距离太大,为了拉平两者的速度差,现代计算机会在两者之间刺进一块速度比内存更快的高速缓存,CPU 缓存是分级的,有 L1 / L2 / L3 三级缓存。其间 L1 / L2 缓存是中心独占的,而 L3 缓存是多中心同享的。

在 CPU Cache 的三级缓存中,会存在 2 个缓存共同性问题:

  • 纵向 – Cache 与内存的共同性问题:经过写直达或写回战略处理;
  • 横向 – 多中心 Cache 的共同性问题:经过 MESI 等缓存共同性协议处理。

MESI 协议能够满意写传达和业务串行化 2 点特性,经过 “已修正、独占、同享、已失效” 4 个状况完成了 CPU Cache 的共同性;

现代 CPU 为了进步并行度,会在增加写缓冲区 & 失效行列将 MESI 协议的恳求异步化,这其实是一种处理器等级的指令重排,会破坏了 CPU Cache 的共同性。

Cache 不共同问题

已经有 MESI 协议,为什么还需要 volatile 关键字?

MESI 协议在线模拟

已经有 MESI 协议,为什么还需要 volatile 关键字?

网站地址:www.scss.tcd.ie/Jeremy.Jone…

现在,咱们的问题是:既然 CPU 现已完成了 MESI 协议,为什么 Java 言语层面还需求界说 volatile 关键字呢?岂不是多此一举?你或许会说因为写缓冲区和失效行列破坏了 Cache 共同性。好,那不考虑这个因素的话,还需求界说 volatile 关键字吗?

其实,MESI 处理数据共同性(Data Conherence)问题,而 volatile 处理次序共同性(Sequential Consistency)问题。 WC,这两个不相同吗?

已经有 MESI 协议,为什么还需要 volatile 关键字?


2. 数据共同性 vs 次序共同性

2.1 数据共同性

数据共同性评论的是同一份数据在多个副本之间的共同性问题, 你也能够了解为多个副本的状况共同性问题。例如内存与多中心 Cache 副本之间的共同性,或许数据在主从数据库之间的共同性。

当咱们从 CPU 缓存共同性问题开始,逐步评论到 Cache 到内存的写直达和写回战略,再评论到 MESI 等缓存共同性协议,从始至终咱们评论的都是 CPU 缓存的 “数据共同性” 问题,仅仅为了简洁咱们从没有故意强调 “数据” 的概念。

数据共同性有强弱之分:

  • 强数据共同性: 确保在恣意时刻恣意副本上的同一份数据都是相同的,或许答应不同,可是每次运用前都要刷新确保数据共同,所以终究仍是共同。
  • 弱数据共同性: 不确保在恣意时刻恣意副本上的同一份数据都是相同的,也不要求运用前刷新,可是随着时刻的搬迁,不同副本上的同一份数据总是向趋同的方向变化,终究仍是趋向共同。

例如,MESI 协议便是强数据共同性的,但引入写缓冲区或失效行列后就变成弱数据共同性,随着缓冲区和失效行列被消费,各个中心 Cache 终究仍是会趋向共同状况。

已经有 MESI 协议,为什么还需要 volatile 关键字?

2.2 次序共同性

次序共同性评论的是对多个数据的屡次操作次序在整个体系上的共同性。在并发编程中,存在 3 种指令次序:

  • 编码次序(Progrom Order):源码中指令的编写次序,是程序员视角看到的指令次序,不一定是实践履行的次序;
  • 履行次序(Memory Order): 指单个线程或处理器上实践履行的指令次序;
  • 大局履行次序(Global Memory Order): 每个线程或处理器上看到的体系整体的指令次序,在弱次序共同性模型下,每个线程看到的大局履行次序或许是不同的。

次序共同性模型是计算机科学家提出的一种抱负参阅模型,为程序员描绘了一个极强的大局履行次序共同性,由 2 个特性组成:

  • 特性 1 – 履行次序与编码次序共同: 确保每个线程中指令的履行次序与编码次序共同;
  • 特性 2 – 大局履行次序共同: 确保每个指令的成果会同步到主内存和各个线程的作业内存上,使得每个线程上看到的大局履行次序共同。

举个比如,线程 A 和线程 B 并发履行,线程 A 履行 A1 → A2 → A3,线程 B 履行 B1 → B2 → B3。那么,在次序共同性内存模型下,尽管程序整体履行次序是不确定的,可是线程 A 和线程 B 总会依照 1 → 2 → 3 编码次序履行,而且两个线程总能看到相同的大局履行次序。

次序共同性内存模型

已经有 MESI 协议,为什么还需要 volatile 关键字?

2.3 弱次序共同性(一定要了解)

尽管次序共同性模型对程序员十分友爱,可是对编译器和处理器却不见得脍炙人口。假如程序完全依照次序共同性模型来完成,那么处理器和编译器的许多重排序优化都要被禁止,这对程序的 “并行度” 会有影响。例如:

  • 1、重排序问题: 编译器和处理器不能重摆放没有依靠联系的指令;
  • 2、内存可见性问题: CPU 不能运用写回战略,也不能运用写缓冲区和失效行列机制。其实,从内存的视角看也是指令重排问题。

所以,在 Java 虚拟机和处理器完成中,实践上运用的是弱次序共同性模型:

  • 特性 1 – 不要求履行次序与编码次序共同: 不要求单线程的履行次序与编码次序共同,只要求履行成果与强次序履行的成果共同,而指令是否真的按编码次序履行并不关心。因为成果不变,从程序员的视角看程序便是按编码次序履行的假象;
  • 特性 2 – 不要求大局履行次序共同: 答应每个线程看到的大局履行次序不共同,乃至答应看不到其他线程已履行指令的成果。

举个单线程的比如: 在这段计算圆面积的代码中,在弱次序共同性模型下,指令 A 和 指令 B 能够不按编码次序履行。因为 A 和 B 没有数据依靠,所以对终究的成果也没有影响。可是 C 对 A 和 B 都有数据依靠,所以 C 不能重摆放到 A 或 B 的前面,不然会改动程序成果。

伪代码

double pi = 3.14; // A
double r = 1.0// B
double area = pi * r * r; // C(数据依靠于 A 和 B,不能重摆放到前面履行)

指令重排

已经有 MESI 协议,为什么还需要 volatile 关键字?

再举个多线程的比如: 咱们在 ChangeThread 线程修正变量,在主线程调查变量的值。在弱次序共同性模型下,答应 ChangeThread 线程 A 指令的履行成果不及时同步到主线程,在主线程看来就像没履行过 A 指令。

这个问题咱们一般会了解为内存可见性问题,其实咱们能够统一了解为次序共同性问题。 主线程看不到 ChangeThread 线程 A 指令的履行成果,就好像两个线程看到的大局履行次序不共同:ChangeThread 线程看到的大局履行次序是:[B],而主线程看到的大局履行次序是 []。

可见性示例程序

public class VisibilityTest {
    public static void main(String[] args) {
        ChangeThread thread = new ChangeThread();
        thread.start();
        while (true) {
            if (thread.flag) { // B
                System.out.println("Finished");
                return;
            }
        }
    }
    public static class ChangeThread extends Thread {
        private boolean flag = false;
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true; // A
            System.out.println("Change flag = " + flag);
        }
    }
}

程序输出

Change flag = true
// 无限等待

前面你说到编译器和处理器的重排序,为什么指令能够重排序,为什么重排序能够进步功能,重排序不会出错吗?


3. 什么是指令重排序?

3.1 重排序类型

从源码到指令履行一共有 3 种等级重排序:

  • 1、编译器重排序: 例如将循环内重复调用的操作提前到循环外履行;
  • 2、处理器体系重排序: 例如指令并行技能将多条指令重叠履行,或许运用分支猜测技能提前履行分支的指令,并把计算成果放到重摆放缓冲区(Reorder Buffer)的硬件缓存中,当程序真的进入分支后直接运用缓存中的结算成果;
  • 3、存储器体系重排序: 例如写缓冲区和失效行列机制,即是可见性问题,从内存的角度也是指令重排问题。

指令重排序类型

已经有 MESI 协议,为什么还需要 volatile 关键字?

3.2 什么是数据依靠性?

编译器和处理器在重排序时,会遵从数据依靠性准则,不会企图改动存在数据依靠联系的指令次序。假如两个操作都是访问同一个数据,并且其间一个是写操作,那么这两个操作就存在数据依靠性。此刻一旦改动次序,程序终究的履行成果一定会发生改动。

数据依靠性分为 3 种类型::

数据依靠性 描绘 示例
写后读 写一个数据,再读这个数据 a = 1; // 写
b = a; // 读
写后写 写一个数据,再写这个数据 a = 1; // 写
a = 2; // 写
读后写 读一个数据,再写这个数据 b = a; // 读
a = 1; // 写

3.3 指令重排序安全吗?

需求留意的是:数据依靠性准则只对单个处理器或单个线程有效,因此即便在单个线程或处理器上遵从数据依靠性准则,在多处理器或许多线程中依然有或许改动程序的履行成果。

举例说明吧。

比如 1 – 写缓冲区和失效行列的重排序: 假如是在一个处理器上履行 “写后读”,处理器不会重排这两个操作的次序;但假如是在一个处理器上写,之后在另一个处理器上读,就有或许重排序。关于写缓冲区和失效行列引起的重排序问题,上一篇文章现已解说过,不再重复。

写缓冲区形成指令重排

已经有 MESI 协议,为什么还需要 volatile 关键字?

比如 2 – 未同步的多线程程序中的指令重排: 在未同步的两个线程 A 和 线程 B 上别离履行这两段程序,程序的预期成果应该是 4,但实践的成果或许是 0

线程 A

a = 2; // A1
flag = true; // A2

线程 B

while (flag) { // B1
    return a * a; // B2
}

状况 1:因为 A1 和 A2 没有数据依靠性,所以编译器或处理器或许会重排序 A1 和 A2 的次序。在 A2 将 flag 改为 true 后,B1 读取到 flag 条件为真,并且进入分支计算 B2 成果,但 A1 还未写入,计算成果是 0。此刻,程序的运转成果就被重摆放破坏了。

状况 2:另一种或许,因为 B1 和 B2 没有数据依靠性,CPU 或许用分支猜测技能提前履行 B2,但 A1 还未写入,计算成果仍是 0。此刻,程序的运转成果就被重摆放破坏了。

多线程的数据依靠性不被考虑

已经有 MESI 协议,为什么还需要 volatile 关键字?

小结一下: 重排序在单线程程序下是安全的(与预期共同),但在多线程程序下是不安全的。


4. 答复开始的问题

到这儿,尽管咱们的评论还未完毕,但现已满足答复标题的问题:“现已有 MESI 协议,为什么还需求 volatile 关键字?”

即便不考虑写缓冲区或失效行列,MESI 也仅仅处理数据共同性问题,并不能处理次序共同性问题。在实践的计算机体系中,为了进步程序的功能,Java 虚拟机和处理器会运用弱次序共同性模型。

在单线程程序下,弱次序共同性与强次序共同性的履行成果完全相同。但在多线程程序下,重排序问题和可见性问题会导致各个线程看到的大局履行次序不共同,使得程序的履行成果与预期不共同。

为了纠正弱次序共同性的影响,编译器和处理器都提供了 “内存屏障指令” 来确保程序关键节点的履行次序能够与程序员的预期共同。在高档言语中,咱们不会直接运用内存屏障,而是运用更高档的语法,即 synchronized、volatile、final、CAS 等语法。

那么,什么是内存屏障?synchronized、volatile、final、CAS 等语法和内存屏障有什么关联,这个问题咱们鄙人一篇文章打开评论,请关注。


参阅资料

  • Java 并发编程的艺术(第 1、2、3 章) —— 方腾飞 魏鹏 程晓明 著
  • 深化了解 Android:Java 虚拟机 ART(第 12.4 节) —— 邓凡平 著
  • 深化了解 Java 虚拟机(第 5 部分) —— 周志明 著
  • 深化浅出计算机组成原理(第 55 讲) —— 徐文浩 著,极客时刻 出品
  • CPU有缓存共同性协议(MESI),为何还需求 volatile —— 一角钱技能 著
  • 一文读懂 Java 内存模型(JMM)及 volatile 关键字 —— 一角钱技能 著
  • MESI protocol —— Wikipedia
  • Cache coherence —— Wikipedia
  • Sequential consistency —— Wikipedia
  • Out-of-order execution —— Wikipedia
  • std::memory_order —— cppreference.com

已经有 MESI 协议,为什么还需要 volatile 关键字?