JAVA内存模型

文章目录

  • 1.概述
  • 2.JAVA内存模型
    • 2.1.主内存与工作内存
    • 2.2.内存间交互
    • 2.3.volatile变量的特殊规则
    • 2.4.非原子性协议(long和double型变量的特殊规则)
    • 2.5.原子性、可见性与有序性
    • 2.6.先行发生(happens-before)原则

1.概述

java内存模型与物理内存有不少相似之处,任何的运算都需要内存与处理器的交互,而内存与处理器的运算速度却相差几个数量级,因此物理内存与处理器之间出现了高速缓存,用来缓冲内存与处理器之间的运算速度的关系,但是引入高速缓存的同时又出现了缓存一致性的问题,即在多处理器中每个处理器都有自己的高速缓存,但又同时共享同一个主内存区域,如果出现多个任务共用同一个主内存区域,将会出现各自的缓存数据不一致的情况。为了解决这个问题需要各个处理器访问缓存时遵循一些协议,在读写时根据协议来进行操作,它们之间的关系如下图所示。
JAVA内存模型_第1张图片
java内存模型是java虚拟机用来屏蔽掉各种硬件和操作系统的内存访问的差异的手段,以实现java程序在各种平台下都能达到一致的并发效果。

2.JAVA内存模型

java内存模型(JAVA Memory Model,JMM)的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处变量包含实例字段、静态字段和构成数组对象的元素。

2.1.主内存与工作内存

java内存模型规定所有的变量都存储在主内存中,每条线程还有自己的工作内存(主内存和工作内存都是JVM的内存的一部分)。线程的工作内存中保存了被该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,而不能直接读取主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递必须通过主内存来完成,三者的关系如下:
JAVA内存模型_第2张图片

2.2.内存间交互

主内存与工作内存之间的交互,JMM中定义了八种操作来完成:

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

如果把变量从主内存复制到工作内存,需要顺序执行read和load操作,如果把变量从工作内存同步回主内存,要顺序执行store和write操作。JMM要求顺序执行,去没有要求连续执行,即允许read a、read b、load b、load a等操作。此外JMM还规定了操作必须满足如下规则

  1. read和load操作、store和write操作必须同时出现。
  2. assign操作后的变量必须同步回主内存。
  3. 不允许线程没有经过assign操作把数据从工作内存同步回主内存。
  4. 新变量只能从主内存产生,不允许工作内存使用未被load和assign的变量,即use之前必须load,store之前必须assign。
  5. 一个变量同一时刻只能被一条线程lock,但是lock可以被同一线程执行多次,同样必须执行相同次数unlock,变量才会被解锁。
  6. 对一个变量执行lock操作,会先清空工作内存给中的变量并使用load或assign操作重新赋值。
  7. 如果一个变量没有被lock,则不允许对它执行unlock,也不允许unlock其他线程的变量。
  8. 对一个变量执行unlock之前,必须先执行store和write操作将此变量同步回主内存。

2.3.volatile变量的特殊规则

volatile是java虚拟机提供的最轻量级的同步机制,它有两个特性:

  • 保证变量对所有线程的可见性
    保证可见性需要满足两个规则,如下:

    • 运算结果不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
    • 变量不需要与其他的状态变量共同参与不变约束。
  • 禁止指令重排序优化
    普通变量仅仅保证在该方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致(指令重排序影响)。而volatile变量能避免指令重排序保证变量赋值的顺序与代码中的执行顺序一致。

  • volatile实现原理
    volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM 采取保守策略。下面是基于保守策略的 JMM 内存屏障插入策略:

    • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
    • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
    • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
    • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的 volatile 内存语义。

Map configOptions;
char[] config;
volatile boolean initialized = false;

Thread threadA = new Thread(new Runnable() {
    @Override
    public void run() {
    	//初始化配置文件
    	//初始化完成后将initialized设为true
        configOptions = new HashMap();
        config = readConifgFile("config.xml");
        processConfigOptions(config,configOptions);
        initialized = true;
    }
});
Thread threadB = new Thread(new Runnable() {
    @Override
    public void run() {
        while(!initialized){
        	sleep();
        }
        //使用线程A中初始化的配置文件
        ...
    }
});
threadA.start();
threadB.start();

以上代码如果initialized变量没有定义为volatile,就可能由于指令重排序出现initialized = true先执行,而读取配置信息后执行的情况,此时线程B读取initialized 状态往后执行就会出现错误。而volatile就能避免此类情况发生。

2.4.非原子性协议(long和double型变量的特殊规则)

java内存模型要求lock、unlock、read、load、assign、use、store、write这八个操作都具有原子性,但是对于64位的数据类型(long和double),在模型中特别定义了一条规则:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read、write这四个操作的原子性,这就是非原子性协议。

2.5.原子性、可见性与有序性

  • 原子性
    由java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write这留个操作,我们可以认为基础数据类型的访问读写是具备原子性的,非原子性协议的long和double除外。而lock和unlock则是通过字节码执行monitorenter和monitorexit来使用,对应代码就是synchronized来实现,因此synchronized块也具备原子性。
  • 可见性
    可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。JMM是通过主内存和工作内存之间的八种操作来实现变量值的同步,能实现可见性的操作有volatile、synchronized、final等。
    volatile是通过其特殊规则能够保证新值能立即同步到主内存,及每次使用前立即从注内存刷新,因此保证了多线程操作时变量的可见性。
    synchronized的可见性是指对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)来保证。
    final可见性是指被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么在其他线程中就能看见final字段的值。
  • 有序性
    如果在线程内观察,所有的操作都是有序的(线程内表现为串行的语义);如果在一个线程中观察另一个线程,所有的操作都是无序的(执行重排序、工作内存与主内存同步延迟)。

2.6.先行发生(happens-before)原则

java中的有序性除了靠volatile和synchronized来完成之外,还有一个先行发生原则,满足这些规则的操作就能保证有序性。

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

参考资料:
《深入理解JAVA虚拟机》
https://www.infoq.cn/article/java-memory-model-4

你可能感兴趣的:(深入理解JVM)