JVM相关及其拓展(五) -- Java内存模型

Java内存模型

屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到的一致的内存访问效果。

主要目标:

定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。变量包括了实例字段、静态字段和构成对象的元素,但不包括局部变量和方法参数(他们为线程私有,不被共享)。

Java内存模型

主内存与工作内存

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Work Memory)。工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)必须在工作内存中进行,不能直接读取主内存的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间传递变量均需通过主内存完成。

线程-主内存-工作内存的交互关系

主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。

内存间交互操作

关于主内存与工作内存具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,JMM中定义了8种操作来完成.

每种操作都是原子、不可再分

类型 说明
Lock(锁定) 作用于主内存的变量,把一个变量表示为一条线程独占的状态
Unlock(解锁) 作用于主内存的变量,把一个锁定状态的变量释放出来
Read(读取) 作用于主内存的变量,一个变量值从主内存传输到线程的工作内存中
Load(载入) 作用于工作内存的变量,从read操作中得到的值放入工作内存的变量副本中
Use(使用) 作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行
Assign(赋值) 作用于工作内存的变量,把接收到的值赋值给工作内存中的变量,遇到需要赋值的情况会执行
Store(存储) 作用于工作内存的变量,把工作内存中的变量值传到主内存中
Write(写入) 作用于主内存的变量,把store操作中得到的工作内存中的变量的值放入主内存的变量中

如果要把一个变量从主内存复制到工作内存,就要顺序执行readload操作,如果要从工作内存同步回主内存,就要顺序的执行storewrite操作。

原子性、可见性和有序性

Java内存模型就是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的。这也是并发编程的三大概念。

原子性(Atomicity)

对基本数据类型的读取和赋值都是原子操作,所谓原子性操作就代指这些操作是不可中断的,要么做完,要么就不执行。

Java内存模型只保证了基本读取和赋值是原子操作。如果要实现更大范围操作的原子性,就需要通过synchronizedlock实现。

Java中的原子操作包括:

  • 除long和double之外的基本数据类型赋值操作 long和double占用的字节数是8即64bit,在32位操作系统上去读写数据需要两步完成,每一步取32位数据。需要添加volatile关键字保证
  • 所有引用reference的赋值操作
  • java.concurrent.Atomic.* 包下所有类的操作

可见性(Visibility)

当一个线程修改了共享变量的值,其他线程能够立即得知这个值的修改。

Java内存模型通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式实现可见性。

当一个共享变量被volatile修饰时,他会保证修改的值会立即更新到主内存,当有其他线程需要读取时,会立即从主内存中读取新值。

通过synchronizedlock也可保证可见性,保证同一时刻只有一个线程获取锁然后执行代码,并且在释放锁之前会将变量的修改刷新到主内存中,保证可见性。

拓展:final也可以实现可见性,final修饰字段一旦初始化完成,在其他线程中就可以看到fianl的值。

有序性(Ordering)

程序执行的顺序按照代码的先后顺序执行

Java内存模型允许编译器和处理器对指令进行重排序,但是规定了as-if-serial(不管怎么重排序,程序的执行结果不能改变)。

指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

volatile本身包含了禁止指令重排序的语义,而synchronized通过一个变量在同一个时刻只允许一条线程对其lock操作实现有序性。

要想并发程序正确的执行,必须要保证原子性、可见性和有序性,只要有一个没有被保证,就有可能导致程序运行不正确。

先行发生原则(happens-before)

JMM具备一些先天的有序性不需要通过任何手段就可以保证有序性,称之为先行发生原则。如果两个操作的执行次序无法从先行发生原则推导出来,他们之间就没有顺序性保障,就不能保证有序性。

主要有以下规则:

  • 程序次序规则:写在前面的代码先行发生于写在后面的(按照控制流顺序而不是代码顺序)
  • 管程锁定规则:一个解锁操作先行于时间后面发生的同一个线程的加锁操作
  • volatile变量规则:对一个volatile变量的写操作先行发生于读操作
  • 线程启动规则:Thread对象的start()优先于该线程的任意操作
  • 传递性:如果操作A早于B,B又早于C,则A早于C
  • 线程中断规则:线程interrupt()调用早于该线程的中断检测。Thread.interrupted()
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。Thread.join()或者Thread.isAlive()
  • 对象终结规则:一个对象的初始化完成早于finalize()

JMM相关讨论