侧边栏壁纸
  • 累计撰写 98 篇文章
  • 累计创建 20 个标签
  • 累计收到 3 条评论

JMM内存模型

林贤钦
2020-05-21 / 0 评论 / 13 点赞 / 762 阅读 / 0 字
温馨提示:
本文最后更新于 2020-05-25,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

JMM内存模型

1、概念

说到JMM内存模型,先区分一下JMM内存模型和JVM内存结构的区别

  • JVM内存结构

    JVM内存结构,讲的是Java虚拟机内存的结构划分,包括堆区,栈区,方法区等。

  • JMM内存模型

    Java内存模型(JMM),Java Memory Model,指的在java程序运行过程中,计算机有主内存,每个java线程有自己的工作内存。java线程的工作内存是计算机主内存的拷贝。

    它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。

2、JMM目标

定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节(这里变量指代的是实例字段、静态字段和构成数组对象的元素)

3、JMM主要解决的问题

解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题

  • 缓存一致性问题其实就是可见性问题。
  • 处理器优化是可以导致原子性问题
  • 指令重排即会导致有序性问题

4、主内存与工作内存

在JMM的规定中

  • 所有变量均存储在主内存(虚拟机内存的一部分)
  • 每个线程都对应着一个工作线程,主内存中的变量都会复制一份到每个线程的自己的工作空间,线程对变量的操作都在自己的工作内存中,操作完成后再将变量更新至主内存
  • 其他线程再通过主内存来获取更新后的变量信息,即线程之间的交流通过主内存来传递

下面用张图说明

JMM的空间划分和JVM的内存划分不一样,非要对应的话,关系如下

(1)JMM的主内存对应JVM中的堆内存对象实例数据部分

(2)JMM的工作内存对应JVM中栈中部分区域

如果线程A与线程B之间要通信的话,必须要经历下面2个步骤

(1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。

(2)线程B到主内存中去读取线程A之前已更新过的共享变量。

Java中JMM内存模型定义了八种操作来实现同步的细节

  • read 读取,作用于主内存把变量从主内存中读取到本本地内存。
  • load 加载,主要作用本地内存,把从主内存中读取的变量加载到本地内存的变量副本中
  • use 使用,主要作用本地内存,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。、
  • assign 赋值 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store 存储 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write 写入 作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
  • lock 锁定 :作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock 解锁:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

同时在Java内存模型中明确规定了要执行这些操作需要满足以下规则

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

主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,我觉得可以看周志明写的《深入理解java虚拟机》,里面解释的很细节

5、原子性、可见性与有序性

了解了线程,主内存,工作内存的关系,**java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存。**但是每个线程都有自己的工作内存,在多线程高并发的环境下,如何保证线程工作的正确性?

而Java内存模型就是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的,下面介绍这三种特征

5.1、原子性(Atomicity)

原子性操作就是指这些操作是不可中断的,要做一定做完,要么就没有执行,也就是不可被中断

举个简单例子,对于变量x,进行加1,然后取到值,这一个过程尽管简单,但是却不具备原子性,因为我们要先读取x,之后进行计算,然后重新写入,其实是几个步骤。如果仅仅对x进行赋值,那么则可以认为是原子的。

解决:使用 synchronized 或者lock互斥锁来保证操作的原子性

5.2、可见性(Visibility)

可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。

主要有有三种实现可见性的方式:

  • volatile:会强制将该变量自己和当时其他变量的状态都刷出缓存

    volatile 只能保证可见性,不能保证原子性

  • synchronized:对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。

  • final:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。

5.3、有序性(ordering)

在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序

在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

编译器代码重排:出于性能优化的目的,编译器可能在编译的时候对生成的目标代码进行重新排列。

有两种解决办法

  1. volatile: 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
  2. synchronized: 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。

先行发生原则(Happen-Before)

happens-before 规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见

  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见

  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或t1.join()等待它结束)

  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)

  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

  • 具有传递性,如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出 操作A先行发生于操作C的结论。

13

评论区