引言

这篇文章翻译自http://tutorials.jenkov.com/java-concurrency/java-memory-model.html,主要讲述java内存模型。
java内存模型规定了java虚拟机怎样与机器的内存(RAM)一起工作。如果你想设计出正确行为的并发程序,理解java内存模型是非常重要的。Java内存模型对顶了不同的线程什么时候、怎么样看到其他线程写入到共享变量中的值,还有怎么样来对共享变量进行同步访问。
最初的Java内存模型并不成熟,所以在java1.5时进行了修订。这个版本的Java内存模型直到今天还在使用。

内部的Java内存模型

JVM内部的Java内存模型将内存划分为线程栈(thread stack)和堆(heap)。下图展示了Java内存模型的一个逻辑视图:
jmm.png
JVM中每个运行中的线程都有它自己的线程栈(thread stack)。这个线程栈包含了当前线程为了达到当前的执行点而调用的方法的信息。我更偏向于称它为调用栈(call stack),因为随着线程执行代码,调用栈会发生变化。
线程栈同样包含每个要执行的方法的局部变量。一个线程只能访问他自己的线程栈。一个线程创建的局部变量对其他线程不可见。即使两个线程是在执行同样的代码,这两个线程会分别在它们各自的线程栈中创建局部变量。因此,每个线程都有它自己的一份局部变量。
所有的简单类型的局部变量(boolean、byte、short、char、int、long、float、double)会被完全保存在线程栈里(简单类型没有引用的概念,简单类型变量值直接就是简单类型值),因此对其他线程是不可见的。一个线程可以传递一个简单类型变量的copy给到另外一个线程,但是不能分享分享这个变量本身。
而堆内存包含了你的应用中创建的全部对象,不管是哪个线程创建的这个对象。这些对象包括原始类型的包装版本(Byte、Integer、Long等)。不管一个对象是否被局部变量创建和访问,或者它是另一个对象的一个成员变量,它都会在被存储在堆上。
下图描述了我们上面所说的:
stackAndHeap.png
一个局部变量可以是一个简单类型,这种情况下,它会被存储在线程栈上,栈上的值就是简单类型的值。
一个局部变量也可以是到一个对象的引用。这样的话这个局部变量也就是这个引用被存储在线程栈上,但是对象本身是存储在堆上面的。
一个对象可能包含方法,方法可能会有自己的局部变量。这些局部变量同样是存储在线程栈里,即使方法所属的对象是在堆上的。
一个对象的成员变量是跟对象一起存储在堆上的(你需要了解对象的内存布局),不管成员变量是简单类型还是到另外一个的引用,这都成立。
类的静态变量也是存储在堆上的,不过是跟class的定义存储在一起(方法区)。
在堆上的对象能被所有的线程访问,只要线程有一个到这个对象的引用。当一个线程访问一个对象,它也就能够访问那个对象的成员变量。如果两个线程调用同一个对象的一个方法,就意味着他们都能访问这个对象的成员变量,但是每个线程会有它自己的局部变量的copy。

硬件内存架构

现代的硬件内存架构有点与java内存模型不同。理解硬件的内存架构也很重要。接下来我们介绍硬件内存架构,之后我们介绍JAVA内存模型怎样与硬件内存架构配合工作。
下图展示了一个简化的现代机器的硬件架构:
hardWare.png
一个现代计算机经常有两个或者更多的cpu,一些cpu也可能会有多个核。这意味着,在一个现代计算机上,可能会同时有多于一个线程在运行。每个CPU都能在特定时间运行一个线程。也就是说,如果你的java应用程序是多线程的,每个cpu上可以执行一个线程。
每个cpu包含一系列的寄存器,CPU能够在这些寄存器上执行比主内存更快的操作,因为cpu访问寄存器比访问主内存更快。
每个cpu也能有一个CPU缓存层。实际上,大多数的现代CPU都有一定大小的缓存层。CPU访问缓存也比访问主内存要快,但是不会比访问寄存器快。所以,cpu缓存的速度位于寄存器和主内存之间。一些CPU会有多级缓存(Level1和Level2),这个并不重要。我们只需要知道CPU会有缓存就行了。
一个计算器同样会有一个主内存(RAM)。所有的cpu能够访问主内存。主内存一般来说比缓存要大很多。
一般情况下,当一个cpu需要访问主内存,它会读取一部分主内存的数据到缓存,甚至可能读取一部分缓存到它内部的寄存器然后在寄存器上执行操作。当一个CPU需要将结果写回主内存时,它会刷新寄存器的值到缓存,然后再某些时间点将缓存中的值刷新到主内存。
一般当CPU需要在缓存中保存其他数据的时候才会将缓存中的数据刷新到主内存。CPU缓存可能会在某一时刻有数据需要写回到主内存但是会在另一个时刻才会执行刷新动作将数据刷新到主内存。它不必须在缓存更新时每次都读或者写整个缓存。一般来说,缓存会以缓存行为单位进行更新。一个或多个缓存行数量的数据会被cpu从主内存读取到缓存,同样,一个或多个缓存行可能会被cpu从缓存刷新到主内存。

Java内存模型与硬件内存架构之间的连接

我们已经看到,java内存模型和硬件内存模型架构是不同的。硬件内存架构不会区分线程栈和堆,在硬件上,每个线程栈和堆都是在主内存(RAM)上,部分的线程栈和堆也可能在CPU缓存和CPU寄存器上。这个结构可以用下图表示:
bridge.png
当对象和变量能被存储在计算机上不同的内存区域(寄存器、缓存和主存)的时候,一些问题就出现了,两个主要的问题如下:

  • 线程对共享变量的更新(写入)。
  • 读取、检查和写入共享变量的竞态条件。

这两个问题会在下面的章节解释。