天天看点

Java内存模型 --- JMMJMM简介内存间的交互Volatile关键字先行发生原则

JMM简介

在物理机上,由于处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。加入高速缓存带来了一个新的问题:缓存致性问题。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题,如下图所示

Java内存模型 --- JMMJMM简介内存间的交互Volatile关键字先行发生原则

 为了提高物理机的性能,除了增加缓存之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入输出代码进行乱序执行优化,即指令重排序。

Java 内存模型是Java虚拟机规范中定义的一个模型,用来试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。

在JDK1.5(实现了JSR-133)发布后,Java内存模型已经成熟和完善起来了。

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。Java内存模型分为主内存和工作内存两部分,规定所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。如下图所示

Java内存模型 --- JMMJMM简介内存间的交互Volatile关键字先行发生原则

内存间的交互

Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。

Java内存模型 --- JMMJMM简介内存间的交互Volatile关键字先行发生原则
  • lock(锁定):作用于主内存的变量,把一个变量表示为一条线程独占的状态
  • unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,把一个变量的值从主内存传输到工作内存中,以便随后的load操作
  • load(载入):作用于工作内存的变量,在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
  • use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎
  • assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋给工作内存的变量
  • store(存储):作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中,以便随后的write操作
  • write(写入):作用于主内存的变量,在 store 之后执行,把 store 得到的值放入主内存的变量中

除此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:

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

内存模型三大特性

Java的内存模型是围绕着在并发过程中如何处理原子性,可见性和有序性这三个特征来建立的,我们先来看一下这三个特征。

原子性

Java 内存模型保证了 read、load、use、assign、store、write 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。

如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反映到Java代码块中就是syncsynchronize关键字,因此在synchronize块之间的操作也具备原子性。

可见性

可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。

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

  • volatile
  • synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
  • final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。

对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。

有序性

有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的。前半句话是指"线程内表现为串行的语义",后半句是指"指令重排序现象"和"工作内存与主内存同步延迟"现象。

Java语言提供了volatile和synchronize关键字来保证线程之间的有序性。

volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。

 synchronized 保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。

Volatile关键字

volatile关键字是Java虚拟机提供的最轻量级的同步机制,Java内存模型对volatile关键字专门定义了一些特殊的访问规则,主要分为两方面:

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

具体指当一个线程修改了它的值,新值对于其他线程来说是可以立即得知的。实现原理很简单,被volatile修饰的变量每次使用前都要先刷新,即清空工作内存的数据,读取主内存的数据。

需要注意的是,volatile变量在工作内存中不存在一致性问题,但不代表volatile变量的运算在并发环境下是安全的,因为Java的运算并非原子操作,一个++运算包含多个字节码指令,即便只有一个字节码指令,也可能包含多个机器码指令。

  • 禁止指令重排序优化

volatile关键字是通过在指令之前插入内存屏障来实现禁止指令重排序的。

内存屏障:重排序时,不能把内存屏障后面的指令重排序到内存屏障的前面

先行发生原则

如果Java模型中所有的有序性都仅仅依靠vovolatile和synchronize关键字来完成,那么有一些操作将会变得很繁琐,但是我们在编写Java并发代码时并没有感受到这一点,这是因为Java语言中有一个"先行发生"(happens-before)的原则。

先行发生原则指的是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,那么在操作B发生之前,操作A产生的影响能够被操作B观察到,影响包括修改了内存中共享变量的值,发送了消息,调用了方法等。

下面是Java内存模型下一些"天然"先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。

  • 程序次序规则:在一个线程内,按照程序代码顺序,在程序前面的操作先行发生于后面的操作
  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
  • 线程启动规则:Thread对象的start方法发行发生于此线程的每一个动作
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join方法结束
  • 线程中断规则:对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断时间的发生
  • 对象终结规则:一个对象的初始化(构造函数)完成先行发生于它的finalize方法的开始
  • 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C

继续阅读