本文 首发于 🍀 永浩转载 请注明 来源

java内存模型1

一、为什么有java内存模型?

  • 背景

    1. 现有计算机往往是多核的,每个核心下会有高速缓存。高速缓存的诞生是由于「CPU 与内存(主存)的速度存在差异」,L1 和 L2 缓存一般是「每个核心独占」一份的。
    2. 为了让 CPU 提高运算效率,处理器可能会对输入的代码进行「乱序执行」,也就是所谓的「指令重排序」。
    3. 一次对数值的修改操作往往是非原子性的(比如计实际上在计算机执行时就会分成多个指令)

    即:可见性、有序性、原子性

  • 单线程不存在以上问题

    1. 在永远单线程下,上面所讲的均不会存在什么问题,因为单线程意味着无并发。并且在单线程下,编译器/runtime/处理器都必须遵守as-if-serial语义,遵守as-if-serial意味着它们不会对「数据依赖关系的操作」做重排序。
    2. CPU为了效率,有了高速缓存、有了指令重排序等等,整块架构都变得复杂了。我们写的程序肯定也想要「充分」利用CPU的资源啊!于是乎,我们使用起了多线程
  • 多线程线程安全问题

    1. 缓存数据不一致:多个线程同时修改「共享变量」,CPU核心下的高速缓存是「不共享」的,那多个cache与内存之间的数据同步该怎么做?
    2. CPU指令重排序在多线程下会导致代码在非预期下执行,最终会导致结果存在错误的情况。
  • 缓存不一致问题

    1. 使用「总线锁」:某个核心在修改数据的过程中,其他核心均无法修改内存中的数据。(类似于独占内存的概念,只要有CPU在修改,那别的CPU就得等待当前CPU释放)
    2. 缓存一致性协议(MESI协议,其实协议有很多,只是举个大家都可能见过的)。MESI拆开英文是(Modified(修改状态)、Exclusive(独占状态)、Share(共享状态)、Invalid(无效状态))

    缓存一致性协议我认为可以理解为「缓存锁」,它针对的是「缓存行」(CacheIine)进行"加锁",所谓「缓存行」其实就是高速缓存存储的最小单位。MESI协议的原理大概就是:当每个CPU读取共享变量之前,会先识别数据的「对象状态」(是修改、还是共享、还是独占、还是无效)。如果是独占,说明当前CPU将要得到的变量数据是最新的,没有被其他CPU所同时读取。如果是共享,说明当前CPU将要得到的变量数据还是最新的,有其他的CPU在同时读取,但还没被修改。如果是修改,说明当前CPU正在修改该变量的值,同时会向其他CPU发送该数据状态为invalid(无效)的通知,得到其他CPU响应后(其他CPU将数据状态从共享(share)变成invalid(无效)),会当前CPU将高速缓存的数据写到主存,并把自己的状态从modify(修改)变成exclusive (独占)。如果是无效,说明当前数据是被改过了,需 要从主存重新读取最新的数据。

    其实MESI协议做的就是判断「对象状态」,根据「对象状态」做不同的策略

    关键就在于某个CPU在对数据进行修改时,需要「同步」通知其他CPU,表示这个数据被我修改了,你们不能用了。 比较于「总线锁」,MESI协议的"锁粒度"更小了,性能那肯定会更高咯

  • cpu还有优化

    1. 优化思路就是从「同步」变成「异步」。 在修改时会「同步」告诉其他CPU,而现在则把最新修改的值写到「store buffe r」中,并通知其他CPU记得要改状态,随后CPU就直接返回干其他事了。等到收到其它CPU发过来的响应消息,再将数据更新到高速缓存中。其他CPU接收到invalid(无效)通知时,也会把接收到的消息放入「invalid queue」中,只要写到「invalid queue」就会直接返回告诉修改数据的CPU已经将状态置为「invalid」

二、什么是java内存模型

  1. 由于不同CPU架构的缓存体系不一样、缓存一致性协议不一样、重排序的策略不一样、所提供的内存屏障指令也有差异,为了简化Java开发人员的工作。Java封装了一套规范,这套规范就是「Java 内存模型」
  2. 再详细地说,「Java内存模型」希望屏蔽各种硬件和操作系统的访问差异,保证了Java程序在各种平台下对内存的访问都能得到一致效果。
  3. 目的是解决多线程存在的原子性、可见性(缓存一致性)以及有序性问题。

java内存模型它是一种规范,java虚拟机会实现这种规范

三、java内存模型的内容

  1. java内存模型的抽象结构

    定义:

    • java内存模型定义了:java线程对内存数据进行交互的规范。

    • 线程之间的「共享变量」存储在「主内存」中,每个线程都有自己私有的「本地内存」,「本地内存」存储了该线程以读/写共享变量的副本。

    • 本地内存是Java内存模型的抽象概念,并不是真实存在的。

    规定:

    • Java内存模型规定了:线程对变量的所有操作都必须在「本地内存」进行,「不能直接读写主内存」的变量
    • Java内存模型定义了8种操作来完成「变量如何从主内存到本地内存,以及变量如何从本地内存到主内存」,分别是read/load/use/assign/store/write/lock/unlock操作
    • 看着8个操作很多,对变量的一次读写就涵盖了这些操作了,我再画个图给你讲讲

  2. happen-beforeguize

    • 按我的理解下,happen-before实际上也是一套「规则」。Java内存模型定义了这套规则,目的是为了阐述「操作之间」的内存「可见性」。

    • 从上次讲述「指令重排」就提到了,在CPU和编译器层面上都有指令重排的问题。

      指令重排虽然是能提高运行的效率,但在并发编程中,我们在兼顾「效率」的前提下,还希望「程序结果」能由我们掌控的。 说白了就是:在某些重要的场景下,这一组操作都不能进行重排序,「前面一个操作的结果对后续操作必须是可见的」

    • 于是,Java内存模型 happen-就提出了-b efore这套规则,规则总共有8条:比如传递性、 volatile变量规则、程序顺序规则、监视器锁的规则(具体看规则的含义就好了,这块不难)

    • 只要记住,有了 happen-before-这些规则。我们写的代码只要在这些规则下,前一个操作的结果对后续操作是可见的,是不会发生重排序的。

  3. 对volatile内存语义的探讨

  • 嗯,volatile是Java的一个关键字 为什么讲Java内存模型往往就会讲到volatile这个关键字呢,我觉得主要是它的特性:可见性和有序性(禁止重排序

  • Java内存模型这个规范,很大程度下就是为了解决可见性和有序性的问题。

  • Java内存模型为了实现volatile有序性和可见性,定义了4种内存屏障的「规范」,分别是LoadLoad/LoadStore/StoreLoad/StoreStore

  • 回到volatile上,说白了,就是在volatile「前后」加上「内存屏障」,使得编译器和CPU无法进行重排序,致使有序,并且写volatile变量对其他线程可见。

  • Java内存模型定义了规范,那Java虚拟机就得实现啊,是不是?

    • 之前看过Hotspot虚拟机的实现,在「汇编」层面上实际是通过Lock前缀指令来实现的,而不是各种fence指令(主要原因就是简便。因为大部分平台都支持lock指令,而fence指令是x86平台的)。
    • lock指令能保证:禁止CPU和编译器的重排序(保证了有序性)、保证CPU写核心的指令可以立即生效且其他核心的缓存数据失效(保证了可见性)
  • volatile和MESI协议有啥关系?

    • 没啥关系:Java内存模型关注的是编程语言层面上,它是高维度的抽象。

    • MESI是CPU缓存一致性协议,不同的CPU架构都不一样,可能有的CPU压根就没用MESI协议..

    • 只不过MESI名声大,大家就都拿他来举例子了。

    • MESI可能只是在「特定的场景下」为实现volatile的可见性/有序性而使用到的一部分罢了

    • 为了让Java程序员屏蔽上面这些底层知识,快速地入门使用volatile变量

    • Java内存模型的happen-before规则中就有对volatile变量规则的定义: 这条规则的内容其实就是:对一个volatil e变量的写操作相对于后续对这个volatile变量的读操作可见

      它通过happen-before规则来规定:只要变量声明了volatile关键字,写后再读,读必须可见写的值。(可见性、有序性)