Java内存模型与volatile

本文出自:【InTheWorld的博客】

关于内存模型的问题,我以前写过一篇关于硬件内存模型的博客(多核程序设计——存储模型)。这篇blog主要分析了完全存储定序与部分存储定序的硬件存储模型。总的来说,硬件千奇百怪,内存模型五花八本,完全掌握这些CPU的脾气其实是件非常困难的事。Java作为一种跨平台的语言,实现统一的内存模型是非常有必要的。尤其是在并行程序非常常见的今天,一个良好定义的内存模型可以平衡开发效率与运行效率之间的矛盾。

  • Java内存模型

Java内存模型的目的是为了给编程人员提供一个标准的设计接口,那么Java内存模型需要处理的问题有哪些呢?首先,一切问题都起源于人们对运行性能的追求,没有这些优化也就不存在这些问题,然而这是不可能的。Java内存模型需要处理的问题就是“乱序问题”。乱序问题,一般有以下几种情况:

  1. 编译器优化:为了提升程序的执行速度,在不影响单线程语义的前提下,编译器会重新排布指令的顺序
  2. 指令乱序发射:由于指令执行时间长短不一,CPU为了运算性能会调节指令的执行顺序
  3. 内存重排序:由于不同级别的缓存和读写缓冲区的存在,内存访问的顺序也会是乱序

对于编译优化,JMM标准会禁止一些难以处理的优化。而对于指令乱序与内存操作乱序,各种CPU会提供不同程度的内存屏障(Memory Barriers),使用这些内存屏障可以强制保证一定的顺序。

image

上图是一个抽象的SMP系统,各个CPU通过内存控制设备操作内存。如果没有内存屏障,各个CPU之间的指令操作顺序完全无法确定。内存屏障大致可以分为四类:

1). LoadLoad屏障(读内存屏障)

读屏障是一个数据依赖屏障,并且,所有在屏障之前的加载操作将在屏障之 后的加载操作之前发生,同时这个顺序要被系统中其他组件所认可。 读屏障仅仅对加载进行排序,它对存储没有任何效果。

2). StoreStore屏障(写内存屏障)

CPU 可以被视为按时间顺序提交一系列存储操作。所有在写屏障之前的存储将发生在所有屏障之后的存储操作之前。

3). LoadStore屏障

所有在该屏障之前的读取将发生在所有屏障之后的存储操作之前。

4). StoreLoad屏障

所有在StoreLoad屏障之前的存储将发生在所有屏障之后的读取操作之前。

JMM所规定的几种内存屏障与《深入理解并行程序设计》中分类并不一样,我还没有完全理解这两种分类的同时存在的合理性。

  • volatile关键字

volatile关键字是Java多线程程序中常用的关键字。被volatile关键字修饰的变量具有以下特性:

  1. 可见性,对一个volatile变量的读,总是可以看到任意线程对这个变量的最后的写入。
  2. 原子性,对于volatile变量的读写操作是原子性的。

为了实现volatile关键字,JMM规定了如下的指令重排规则表:

image

对这个规则表一个比较保守的实现方法是:

在每个volatile写操作之前插入一个StoreStore屏障,之后插入一个StoreLoad屏障;在每个volatile度操作之后插入一个LoadLoad屏障和一个LoadStore屏障。

 

参考资料:

《深入Java内存模型》

《深入理解并行程序设计》

《The JSR-133 Cookbook for Compiler Writers》

发表评论