你真的了解JMM吗?

引言

在现代计算机中,cpu的指令速度远超内存的存取速度,因为计算机的存储设备与处理器的运算速度有几个数量级的差距,因此现代计算机系统都不得不加入一层读写速度尽量接近处理器运算速度的高速缓存(Cache)来做为内存与处理器之间的缓冲:将运算须要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。php

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,可是也为计算机系统带来更高的复杂度,由于它引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每一个处理器都有本身的高速缓存,而它们又共享同一主内存(MainMemory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能致使各自的缓存数据不一致,举例说明变量在多个CPU之间的共享。若是真的发生这种状况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,须要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操做,这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。java

1、JMM(Java Memory Model)

java虚拟机规范定义java内存模型屏蔽掉各类硬件和操做系统的内存访问差别,以实现让java程序在各类平台下都能达到一致的并发效果。web

java内存模型规定了一个线程如何和什么时候能够看到由其余线程修改事后的共享变量的值,以及在必须时如何同步的访问共享变量编程

注意:咱们这里强调的是共享变量,不是私有变量。缓存

java内存模型规定了全部的变量都存储在主内存中(JVM内存的一部分)。每条线程都有本身的工做内存,工做内存中保存了该线程使用的主内存中共享变量的副本,线程对变量的全部操做(读取、赋值等)都必须在工做内存中进行,而不能直接读写主内存中的变量;工做内存在线程间是隔离的,不能直接访问对方工做内存中的变量。因此在多线程操做共享变量时,就经过JMM来进行控制。安全

咱们来看一看线程,工做内存、主内存三者的交互关系图。多线程

2、JMM的8种内存交互操做

9龙就疑问,JMM是如何保证并发下数据的一致性呢?并发

内存交互操做有8种,虚拟机实现必须保证每个操做都是原子的,不可再分的(对于double和long类型的变量来讲,load、store、read和write操做在某些平台上容许例外)app

  • lock (锁定):做用于主内存的变量,把一个变量标识为线程独占状态。
  • read (读取):做用于主内存变量,它把一个变量的值从主内存传输到线程的工做内存中,以便随后的load动做使用。
  • load (载入):做用于工做内存的变量,它把read操做从主存中获得变量放入工做内存的变量副本中。
  • use (使用):做用于工做内存中的变量,它把工做内存中的变量传输给执行引擎,每当虚拟机遇到一个须要使用到变量的值的字节码指令时将会执行这个操做。
  • assign (赋值):做用于工做内存中的变量,它把一个从执行引擎中接受到的值赋值给工做内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做。
  • store (存储):做用于工做内存中的变量,它把一个从工做内存中一个变量的值传送到主内存中,以便后续的write使用。
  • write  (写入):做用于主内存中的变量,它把store操做从工做内存中获得的变量的值放入主内存的变量中。
  • unlock (解锁):做用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定。

若是是将变量从主内存复制到工做内存,必须先执行read,后执行load操做;若是是将变量从工做内存同步到主内存,必须先执行store,后执行write。JMM要求read和load, store和write必须按顺序执行,但不是必须连续执行,中间能够插入其余的操做。函数

2.一、JMM指令使用规则
  • 不容许read和load、store和write操做之一单独出现。即便用了read必须load,使用了store必须write
  • 不容许线程丢弃他最近的assign操做,即工做变量的数据改变了以后,必须告知主存
  • 不容许一个线程将没有assign的数据从工做内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不容许工做内存直接使用一个未被初始化的变量。就是对变量实施use、store操做以前,必须通过assign和load操做
  • 一个变量同一时间只有一个线程能对其进行lock。屡次lock后,必须执行相同次数的unlock才能解锁
  • 若是对一个变量进行lock操做,会清空全部工做内存中此变量的值,在执行引擎使用这个变量前,必须从新load或assign操做初始化变量的值
  • 若是一个变量没有被lock,就不能对其进行unlock操做。也不能unlock一个被其余线程锁住的变量
  • 对一个变量进行unlock操做以前,必须把此变量同步回主内存

3、volatile

不少并发编程中都使用了volatile,你知道为何一个变量要使用volatile修饰吗?

volatile有两个语义:

  1. volatile能够保证线程间变量的可见性。
  2. volatile禁止CPU进行指令重排序。

volatile修饰的变量,若是某个线程更改了变量值,其余线程能够当即观察到这个值。而普通变量不能作到这一点,变量值在线程间传递均须要主内存来完成。若是线程修改了普通变量值,则须要刷新回主内存,另外一个线程须要从主内存从新读取才能知道最新值。

3.一、volatile只能保证可见性,不能保证原子性

虽然volatile只能保证可见性,但不能认为volatile修饰的变量能够在并发下是线程安全的。

public class VolatileTest {
    /**
     * 进行自增操做的变量
     * 使用volatile修饰
     */

    private static volatile int count;

    public static void main(String[] args) {
        int threadNums = 2000;
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < threadNums; i++) {
            service.execute(VolatileTest::addCount);
        }
        System.out.println(count);
        service.shutdown();
    }

    private static void addCount() {
        count++;
    }
}
//输出结果
//1994
复制代码

咱们能够从例子中看出,共享变量使用了volatile修饰,启动2000个线程对其进行自增操做,若是是线程安全的,结果应该是2000;但结果却小于2000。证实volatile修饰的变量并不能保证原子性,若是想保证原子性,还须要额外加锁。

3.二、volatile禁止指令重排序

虽然程序从表象上看到是按照咱们书写的顺序进行执行,但因为CPU可能会因为性能缘由,对执行指令进行重排序,以此提升性能。

好比咱们有一个方法是关于“谈恋爱”的方法。伪代码以下

{
    //线程A执行1,2,3
    //一、先认识某个女生,有好感
    //二、开展追求
    //三、追求成功

    //线程B,等待线程A追求成功后开始进入甜蜜的爱情
    while(!追求成功){
        sleep();
    }
    //一块儿看电影,吃饭,牵手,接吻,xxx
}
复制代码

咱们看到线程A须要执行3步,因为cpu执行重排序优化,可能执行顺序变为一、三、2,乱套了,刚认识别人就成功了,接着就牵手,接吻,而后可能再执行追求的过程。。。。。。。。不敢想象,我还只是个孩子啊。这就是指令重排序可能在多线程环境下出现的问题。

若是咱们使用volatile修饰“追求成功”的变量,则能够禁止CPU进行指令重排序,让谈恋爱是一件轻松而快乐的事情。

volatile使用内存屏障来禁止指令重排序。
在每一个volatile写操做的前面插入一个StoreStore屏障,在每一个volatile写操做的后面插入一个StoreLoad屏障。

在每一个volatile读操做的后面插入一个LoadLoad屏障,在每一个volatile读操做的后面插入一个LoadStore屏障。

4、原子性、可见性、顺序性

咱们看到JMM围绕这三个特征来创建的。

4.一、原子性

JMM提供了read、load、use、assign、store、write六个指令直接提供原子操做,咱们能够认为java的基本变量的读写操做是原子的(long,double除外,由于有些虚拟机能够将64位分为高32位,低32位分开运算)。对于lock、unlock,虚拟机没有将操做直接开放给用户使用,但提供了更高层次的字节码指令,monitorentermmonitorexit来隐式使用这两个操做,对应于java的synchronized关键字,所以synchronized块之间的操做也具备原子性

4.二、可见性

咱们上面说了线程之间的变量是隔离的,线程拿到的是主存变量的副本,更改变量,须要刷新回主存,其余线程须要从主存从新获取才能拿到变动的值。全部变量都要通过这个过程,包括被volatile修饰的变量;但volatile修饰的变量,能够在修改后强制刷新到主存,并在使用时从主存获取刷新,普通变量则不行。

除了volatile修饰的变量,synchronized和final。synchronized在执行完毕后,进行unlock以前,必须将共享变量同步回主内存中(执行store和write操做)。前面规则其中一条。

而final修饰的字段,只要在构造函数中一旦初始化完成,而且没有对象逃逸(指对象为初始化完成就能够被别的线程使用),那么在其余线程中就能够看到final字段的值。

4.三、有序性

有序性在volatile已经详细说明了。能够总结为,在本线程观察到的结果,全部操做都是有序的;若是多线程环境下,一个线程观察到另外一个线程的操做,就说杂乱无序的。

java提供了volatile和synchronized两个关键字保证线程之间的有序性,volatile使用内存屏障,而synchronized基于lock以后,必须unlock后,其余线程才能从新lock的规则,让同步块在在多线程间串行执行。

5、Happends-Before原则

先行发生是java内存模型中定义的两个操做的顺序,若是说操做A先行发生于线程B,就是说在发生操做B以前,操做A产生的影响能被操做B观察到,“影响”包括修改了内存中共享变量的值,发送了消息,调用了方法等。

咱们举个例子说一下。

//线程A执行
i = 1
//线程B执行
j = i
//线程C执行
i = 2
复制代码

咱们仍是定义A线程执行 i = 1 先行发生于 线程B执行的 j = i;那么咱们能够肯定,在线程B执行以后,j的值是1。由于根据先行发生原则,线程A执行以后,i的值为1,能够被B观察到;而且线程A执行以后,线程B执行以前,没有线程对i的值进行变动。

这时候咱们考虑线程C,若是咱们仍是保证线程A先行发生于B,但线程C出如今A与B之间,那么,你能够肯定j的值是多少吗?答案是否认的。由于线程C的结果也可能被B观察到,这时候多是1,也多是2。这就存在线程安全问题。

在JMM下具备一些自然的先行发生关系,这些原则在无须任何同步协助下就已经存在,能够直接使用。若是两个操做之间的关系不在此列,而且没法从如下先行发生原则推导出来,它们就没有顺序性保证,虚拟机就会进行随意的重排序。

  • 程序次序规则(Program Order Rule):在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。

  • 锁定规则(Monitor Lock Rule):一个Unlock的操做确定先于下一次Lock的操做。这里必须是同一个锁。同理咱们能够认为在synchronized同步同一个锁的时候,锁内先行执行的代码,对后续同步该锁的线程来讲是彻底可见的。

  • volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操做,确定早于后续发生的读操做

  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每个动做

  • 线程终止规则(Thread Termination Rule):Thread对象的停止检测(如:Thread.join(),Thread.isAlive()等)操做,必晚于线程中全部操做

  • 线程中断规则(Thread Interruption Rule):对线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生

  • 对象终止规则(Finalizer Rule):一个对象的初始化方法先于执行它的finalize()方法

  • 传递性(Transitivity):若是操做A先于操做B、操做B先于操做C,则操做A先于操做C

总结

本篇详细总结了Java内存模型。再来品一品这句话。

java内存模型规定了一个线程如何和什么时候能够看到由其余线程修改事后的共享变量的值,以及在必须时如何同步的访问共享变量

各位看官,若是以为9龙的文章对你有帮助,求点赞,求关注。若是转载请注明出处。

本篇主要总结于:

深刻理解Java虚拟机++JVM高级特性与最佳实践

相关文章
相关标签/搜索