JUC - Java内存模型JMM

 2019-12-10 16:18  阅读(952)
文章分类:Java Core

一.概述

C/C++等语言直接使用物理硬件和操作系统的内存模型,因此由于不同平台上内存模型的差异,就必须针对不同的平台开发对应的程序。

Java虚拟机定义了一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统之间的内存访问差异,实现“一次编译,处处运行”的效果。

二.硬件的效率和一致性

众所周知,计算机的存储设备和处理器的运算速度有几个数量级的差距,因此引入了高速缓存,这样处理器就无须等待缓慢的内存读写:

  • 计算时,将需要的数据从内存复制到缓存中,让计算能快速的进行
  • 计算完成后,数据从缓存同步到内存中
    20191210001727\_1.png

高速缓存虽然很好的解决了处理器和内存之间的速度矛盾,但引入了新的问题:

  • 缓存一致性
    在多处理器系统中,每个处理器都有自己的高速缓存,但他们又共享同一个主内存,当多个处理器的运算都涉及到主内存中的某个共享数据,将可能导致缓存数据不一致

三.Java内存模型

Java内存模型主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

3.1 共享变量

上述的变量指的是共享变量:实例变量、静态变量和数组,因为他们存在于Java堆或方法区中,而Java堆和方法区是线程共享的,会出现线程安全问题;而局部变量和方法参数是程序私有的。
20191210001727\_2.png

3.2 Java抽象结构模型

Java内存模型规定了所有共享变量都存储在主内存中,每条线程还要各自的工作内存:

  • 线程的工作内存中保存了被该线程使用变量的主内存副本拷贝
  • 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量
  • 线程对变量的操作完成,将数据重新写回主内存
  • 线程间变量值的传递需要通过主内存来完成
    20191210001727\_3.png

3.3 8种原子操作

Java内存模型定义以下8种原子操作完成了:

  • 一个变量如何从主内存拷贝到工作内存
  • 一个变量如何从工作内存同步回主内存

8种原子操作:

  1. lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态
  2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load操作使用
  4. load(加载):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中
  5. use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用该变量值字节码指令时执行这个操作
  6. assign(赋值):作用于工作内存的变量,把一个执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  7. store(存储):作用于工作内存的变量,把工作内存中变量的值同步回主内存,以便随后的write操作使用
  8. write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量值放入主内存的变量中
    20191210001727\_4.png

3.4 8种操作满足的规则

Java内存模型规定了在执行上述8种基本操作时需要满足以下规则:

  • read和load、store和write操作必须按顺序执行,但不保证连续执行,即2个操作之间可以插入其他指令
  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近的assign操作,即变量在内存中改变了之后必须把该变化同步回主内存
  • 不允许一个线程无原因的(没有发生过任何assign操作)把数据从工作内存同步回主内存
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,即在对一个变量执行use、store操作之前,必须先执行过assign和load操作
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复多次执行,多次执行lock后,只有执行相同次数的unlock操作后,变量才会被解锁
  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或assign操作初始化此变量的值
  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)

3.5 JMM对long和double型变量的特殊规则

JMM规定了lock、unlock、read、load、assign、use、store和write这8种操作具有原子性,但是对于64位的数据类型(long和double),JMM特意规定了一项相对宽松的规定:
允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、 store、 read和write这4个操作的原子性,这点就是所谓的long和double的非原子性协定(Nonatomic Treatment ofdouble andlong Variables)
但是在实际开发中,目前各种平台下的商用虚拟机几乎都把64位数据的读写操作实现为原子操作

四.重排序

为了提高性能,编译器和处理器通常会对指令做重排序。

4.1 重排序分3种类型

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

4.2 数据依赖性

如果2个操作访问同一个变量,且这个操作中有一个为写操作,则这2个操作之间存在数据依赖性
20191210001727\_6.png

4.3 as-if-serial语义

as-if-serial语义:不管怎么重排序,(单线程)程序的执行结果不能被改变。
编译器、runtime和处理器都必须遵守as-if-serial语义,所以编译器和处理器不会对存在数据依赖性的操作执行重排序
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变
as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度

五.volatile关键字

当一个变量被volatile关键字修饰后,它将具备2种特性:

5.1 保证此变量对所有线程的可见性

可见性:当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

  • 当写一个volatile变量时,JMM会把该线程工作内存中变量值刷新到主内存
  • 当读一个volatile变量时,JMM会把该线程工作内存中变量置为无效,从主内存中获取到值

为两位实现volatile变量的可见性,在对volatile变量进行写操作时,汇编代码会多出一行:

lock addl $0x0,(%esp)

Lock前缀的指令在多核处理器会引发2件事:

  • 将当前处理器缓存行的数据写回到主内存
  • 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

5.2 禁止指令重排序优化

普通变量由于重排序,执行顺序和程序代码中顺序可能不一致,volatile则可以禁止指令重排序优化。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。JMM采取了保守策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障
    20191210001727\_7.png
    20191210001727\_8.png
  • StoreStore屏障:禁止上面的普通写和下面的volatile写重排序
  • StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
  • LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
  • LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序

5.3 volatile并不保证在并发下是安全的

volatile变量只能保证可见性,在不符合以下2条规则的运算场景中,任然需要通过加锁(synchronized或juc中的原子类)来保证原子性:

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  • 变量不需要与其他的状态变量共同参与不变约束

六.happens-before先行发生原则

happens-before是JMM最核心的概念。它是判断数据是否存在竞争、线程是否安全的主要依据

6.1 先行发生原则

Java中无须任何同步手段保障就能成立的先行发生规则

  • 程序顺序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确的说,应该是控制流顺序而不是代码顺序,因为要考虑分之、循环等结构
  • 监视器锁规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调是同一个锁,后面是指时间上的先后顺序
  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,后面是指时间上的先后顺序
  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测。可以通过Thead.join()方法结束、Thread.isAlive()的返回值等手段检测线程是否已经终止执行
  • 线程中断规则(Thread Interruption Rule):对线程interrupe()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行完成)先行发生于他的finalize()方法的开始
  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论

20191210001727\_9.png

6.2 先行发生原则示例

假设存在线程A和B,线程A先调用setValue(1)(时间上的先后),然后线程B调用同一对象的getValue(),那B的返回值是什么?

private int value=0;

    pubilc void setValue(int value){
        this.value=value;
    }

    public int getValue(){
        return value;
    }
  • 由于不是同一个线程,所以程序顺序规则不适用
  • 由于没有锁操作,所以监视器规则不适用
  • 由于value变量没有被volatile关键字修饰,所以volatile变量规则不适用
  • 线程启动规则、线程中断规则、线程终止规则和对象终结规则和这里也没半毛钱关系,所以传递性规则也无从谈起

因此,尽管可以判断线程A在时间上先行于线程B,但无法确定线程B的返回结果,换句话说,这里面的操作不是线程安全的。
通过上述:一个操作“时间上的先发生”不代表这个操作会是“x先行发生”。

七.原子性、可见性和有序性

JMM是在围绕并发过程中如何处理原子性、可见性和有序性这3个特征来建立的

7.1 原子性

  • 基本数据类型的原子性:JMM提供了read、load、assign、use、store和write来保证基本数据类型的访问读写具有原子性
  • 更大范围的原子性:JMM提供了lock和unlock操作,字节码指令monitorenter和monitorexit(对应Java关键字synchronized)隐式的使用了lock和unlock操作

7.2 可见性

  • volatile变量:volatile变量读取前从主内存刷新变量值,volatile变量修改后将新值同步回主内存
  • synchronized同步块:对一个变量执行unlock操作之前,必须把此变量同步回主内存中
  • final变量:被final修饰的变量在构造器中初始化完成并且构造器没有把“this”的引用传递出去(this引用逃逸试衣间很危险的事,其他线程有可能通过这个引用访问到“初始化了一般”的对象),那在其他线程中就能看见final变量的值

7.2.1 final域内存语义

对于final域,编译器和处理器要遵守两个重排序规则

  • 写final域的重排序规则会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore障屏

  • 读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障

    public class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample () { i = 1; // 1写final域 obj = this; // 2 this引用在此"逸出" } public static void writer() { new FinalReferenceEscapeExample (); } public static void reader() { if (obj != null) { // 3 int temp = obj.i; // 4 } } }

20191210001727\_10.png

7.3 有序性

  • volatile保证线程间操作的有序性:volatile禁止指令重排序
  • synchronized保证线程间操作的有序性:一个变量同一时刻只允许一条线程对其进行lock操作

参考:
《深入理解Java虚拟机》
《Java并发编程的艺术》

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> JUC - Java内存模型JMM

相关推荐