Java内存模型

并发处理的广泛应用是使得Amdahl定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类“压榨”计算机运算能力的最有力武器。

概述

Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的。Java虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——又称为Java内存模型。

如果你想设计表现良好的并发程序,理解Java内存模型是非常重要的。Java内存模型规定了如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

原始的Java内存模型存在一些不足,因此Java内存模型在Java1.5时被重新修订。这个版本的Java内存模型在Java8中仍在使用。

硬件的效率与一致性

现代计算机在执行并发任务的时候,为了更充分的利用计算机处理器的效能,所以不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache) 来作为内存与处理器之间的缓冲:将运算与要用到的数据复制到缓存中,让运算能够快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

每个CPU可能还有一个CPU缓存层。实际上,绝大多数的现代CPU都有一定大小的缓存层。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。一些CPU还有多层缓存,但这些对理解Java内存模型如何和内存交互不是那么重要。只要知道CPU中可以有一个缓存层就可以了。

通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

当CPU需要在缓存层存放一些东西的时候,存放在缓存中的内容通常会被刷新回主存。CPU缓存可以在某一时刻将数据局部写到它的内存中,和在某一时刻局部刷新它的内存。它不会再某一时刻读/写整个缓存。通常,在一个被称作“cache lines”的更小的内存块中缓存被更新。一个或者多个缓存行可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。

“内存模型”一词可以理解为在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的过程抽象。

JAVA内存模型

JAVA虚拟机规范中视图定义一种JAVA内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让JAVA程序在各种平台下都能够达到一致的内存访问效果。Java1.5发布后,Java内存模型已经成熟和完善起来了。

JVM内部存储结构—堆、线程栈

Java内存模型把Java虚拟机内部划分为线程栈和堆。如下图所示:

栈是运行时的单位,而堆是存储的单位。在Java中每一个运行在Java虚拟机里的线程都拥有自己的线程栈。一个线程仅能访问自己的线程栈。一个线程创建的本地变量对其它线程不可见,即使两个线程执行的是同一段代码,它们也会在各自的线程栈中创建各自的本地变量。

  1. 所有的基本类型的本地变量和对象引用都存放在栈中,因此对其它线程不可见。一个线程可能向另一个线程传递一个基本类型变量的拷贝,但是它不能共享这个基本类型变量自身。
  2. 对象都存放在堆中,包括基本类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。
  3. 一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。
  4. 一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即使这些方法所属的对象存放在堆上。
  5. 一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。
  6. 静态成员变量跟随着类定义一起也存放在堆上。
  7. 存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当多个线程通过调用同一个对象的相同方法访问其成员变量的时候,每个线程都将拥有该成员变量的拷贝而不是其自身。

主内存和工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存中取出变量这样的底层细节。此处的变量(Variables) 与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者不是私有的,不会被共享,自然就不会存在竞争问题。

JAVA内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存(与计算机的高速缓冲类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法访问对方工作内存中的变量,线程间变量值的传递云需要通过主内存来完成。

Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。这种交叉对应关系如下图所示:

当对象和变量被存放在计算机中各种不同的内存区域中时,就可能会出现一些具体的问题。主要包括如下两个方面:

  • 线程对共享变量修改的可见性
  • 当读,写和检查共享变量时出现race conditions

内存间交互操作

交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之间的细节操作。虚拟机保证下列操作都是原子的、不可再分的。

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

线程、工作内存、主内存三者的交互关系如下图所示:

JAVA内存模型规定上述操作必须按顺序执行,但是没有保证是连续执行,也就说其中某连续两个操作之间可以插入其他指令,除此之外JAVA内存模型还规定执行上述操作时需要满足较多规则,规则详情查看 这里不做详细说明。

8种内存访问操作以及上述规定以及完全确定了JAVA程序中那些内存访问操作在并发下是安全的。由于过于严谨,实践起来很麻烦,所以 此篇文章 将介绍这种定义的一个等效判断原则————先行发生原则,来确认一个访问在并发环境下是否安全。

参考

0%