Java内存模型(JMM)
Java虚拟机是整个计算机的模型,因此这个模型自然包括一个内存模型——也就是Java内存模型。Java内存模型指定Java虚拟机如何使用计算机的内存(RAM)。它是一种虚拟机规范,屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。
The Java memory model describes how threads in the java programming language interact through memory. Together with the description of single-threaded execution of code, the memory model provides the semantic of the Java programming language.
Java内存模型描述了java编程语言中的多线程是怎样与内存交互的。Java内存模型与代码的单线程执行情况,一起提供了java编程语言的语义。
The original Java memory model, developed in 1995, was widely perceived as broken,[1] preventing many runtime optimizations and not providing strong enough guarantees for code safety. It was updated through the Java Community Process, as Java Specification Request 133 (JSR-133), which took effect in 2004, for Tiger (Java 5.0).[2][3]
提到Java内存模型,一般指的是JDK 5 开始使用的新的内存模型,主要由JSR-133: JavaTM Memory Model and Thread Specification 描述。
内部结构
JVM内部使用的Java内存模型将内存划分为线程堆栈和堆。
Thread Stack
在Java虚拟机中运行的每个线程都有自己的线程堆栈。线程堆栈包含关于线程调用了哪些方法以到达当前执行点的信息。也称为“调用堆栈”。当线程执行其代码时,调用堆栈会发生变化。
线程堆栈还包含正在执行的每个方法的所有局部变量(调用堆栈上的所有方法)。一个线程只能访问它自己的线程堆栈。线程创建的局部变量除了创建它的线程外,对所有其他线程都是不可见的。即使两个线程正在执行完全相同的代码,这两个线程仍然会在各自的线程堆栈中创建该代码的局部变量。因此,每个线程对每个局部变量都有自己的版本。
所有基本类型的局部变量(boolean, byte, short, char, int, long, float, double)都完全存储在线程堆栈中,因此对其他线程是不可见的。一个线程可以将局部变量的副本传递给另一个线程,但它自己不能共享局部变量。
Heap
堆包含Java应用程序中创建的所有对象,而不管该对象是由哪个线程创建的。这包括局部类型的对象版本(例如Byte、Integer、Long等)。无论对象是创建并赋给局部变量还是作为另一个对象的成员变量创建的,该对象仍然存储在堆上。
存储关系
局部变量可以是基本类型,在这种情况下,它完全保存在线程堆栈中。
局部变量也可以是对象的引用。在这种情况下,引用(局部变量)存储在线程堆栈上,而对象本身则存储在堆上。
对象可以包含方法,而这些方法可以包含局部变量。这些局部变量也存储在线程堆栈中,即使方法所属的对象存储在堆中。
对象的成员变量和对象本身一起存储在堆上。当成员变量是基本类型时,以及当它是对象的引用时,都是如此。
静态类变量也与类定义一起存储在堆上。
堆上的对象可以被所有引用该对象的线程访问。当一个线程可以访问一个对象时,它也可以访问该对象的成员变量。如果两个线程同时调用同一对象上的一个方法,它们都可以访问对象的成员变量,但每个线程都有自己的局部变量副本。
代码示例
public class MyRunnable implements Runnable() {
public void run() {
methodOne();
}
public void methodOne() {
int localVariable1 = 45;
MySharedObject localVariable2 =
MySharedObject.sharedInstance;
//... do more with local variables.
methodTwo();
}
public void methodTwo() {
Integer localVariable1 = new Integer(99);
//... do more with local variable.
}
}
public class MySharedObject {
//static variable pointing to instance of MySharedObject
public static final MySharedObject sharedInstance =
new MySharedObject();
//member variables pointing to two objects on the heap
public Integer object2 = new Integer(22);
public Integer object4 = new Integer(44);
public long member1 = 12345;
public long member2 = 67890;
}
每个执行methodOne()的线程将在各自的线程堆栈上创建自己的localVariable1和localVariable2副本。localVariable1变量将彼此完全分开,只存在于每个线程的线程堆栈中。一个线程不能看到另一个线程对它的localVariable1副本所做的更改。
每个执行methodOne()的线程也将创建它们自己的localVariable2副本。然而,localVariable2的两个不同副本最终都指向堆上的同一个对象。代码将localVariable2设置为指向静态变量引用的对象。静态变量只有一个副本,这个副本存储在堆上。因此,localVariable2的两个副本最终都指向静态变量所指向的MySharedObject的同一个实例。MySharedObject实例也存储在堆上。它对应于上图中的对象3。
请注意MySharedObject类也包含两个成员变量。成员变量本身与对象一起存储在堆上。这两个成员变量指向另外两个Integer对象。这些Integer对象对应于上图中的对象2和对象4。
还要注意methodTwo()如何创建名为localVariable1的局部变量。该局部变量是对Integer对象的对象引用。该方法将localVariable1引用设置为指向一个新的Integer实例。localVariable1引用将存储在每个执行methodTwo()的线程的一个副本中。实例化的两个Integer对象将存储在堆上,但由于每次执行该方法时该方法都会创建一个新的Integer对象,因此执行该方法的两个线程将创建单独的Integer实例。在methodTwo()中创建的Integer对象对应于上图中的对象1和对象5。
还要注意类MySharedObject中的两个成员变量,它们的类型为long,这是一个基本类型。由于这些变量是成员变量,它们仍然与对象一起存储在堆上。只有局部变量存储在线程堆栈上。
现代硬件内存架构
现代计算机通常有两个或更多的cpu。有些cpu可能有多个核。关键是,在拥有2个或更多cpu的现代计算机上,可能有多个线程同时运行。每个CPU在任何给定的时间都能够运行一个线程。这意味着,如果您的Java应用程序是多线程的,那么每个CPU可以同时(并发地)运行Java应用程序中的一个线程。
每个CPU包含一组寄存器,这些寄存器本质上是CPU内存。CPU在这些寄存器上执行操作的速度要比在主内存中执行变量的速度快得多。这是因为CPU访问这些寄存器的速度比它访问主存的速度快得多。
每个CPU还可以有一个CPU缓存内存层。事实上,大多数现代cpu都有一定大小的缓存内存层。CPU可以比主存更快地访问它的高速缓存,但通常没有它访问内部寄存器的速度快。因此,CPU缓存内存的速度介于内部寄存器和主存之间。有些cpu可能有多个缓存层(级别1和级别2),但是要理解Java内存模型如何与内存交互,了解这一点并不重要。重要的是要知道cpu可以有某种类型的缓存内存层。
计算机还包含一个主存储区(RAM)。所有的cpu都可以访问主存。主内存区域通常比cpu的缓存内存大得多。
通常,当CPU需要访问主存时,它会将部分主存数据读入其CPU缓存。它甚至可以将部分缓存读入其内部寄存器,然后对其执行操作。当CPU需要将结果写回主存时,它会将其内部寄存器的值刷新到缓存中,并在某个时刻将该值刷新回主存。
当CPU需要在缓存内存中存储其他内容时,通常会将存储在缓存内存中的值刷新回主内存。CPU缓存可以每次将数据写入部分内存,并每次刷新部分内存。它不必在每次更新时都读/写整个缓存。通常缓存更新在更小的内存块称为“缓存线”。一个或多个高速缓存线路可能被读入高速缓存内存,一个或多个高速缓存线路可能被再次刷新回主存。
Java内存模型和硬件内存架构之间的桥梁
Java内存模型和硬件内存体系结构是不同的。硬件内存体系结构并不区分线程堆栈和堆。在硬件上,线程堆栈和堆都位于主内存中。部分线程堆栈和堆有时会出现在CPU缓存和内部CPU寄存器中。
当对象和变量可以存储在计算机的各种不同的内存区域时,可能会出现某些问题。两个主要问题是:
- 线程更新(写入)对共享变量的可见性。
- 读取、检查和写入共享变量时的竞争条件。
对象可见性
如果两个或多个线程共享一个对象,而没有正确使用volatile声明或同步,那么一个线程对共享对象的更新可能对其他线程不可见。
假设共享对象最初存储在主内存中。然后,运行在CPU 1上的线程将共享对象读入其CPU缓存。在这里,它对共享对象进行了更改。只要CPU缓存没有被刷新回主存,更改后的共享对象版本对运行在其他CPU上的线程是不可见的。这样一来,每个线程都可能拥有共享对象的自己的副本,每个副本位于不同的CPU缓存中。
上图所示,在左侧CPU上运行的一个线程将共享对象复制到其CPU缓存中,并将其count变量更改为2。该更改对于在右边CPU上运行的其他线程是不可见的,因为对count的更新还没有刷新到主内存中。
要解决这个问题,可以使用Java的volatile关键字。volatile关键字可以确保给定的变量直接从主内存中读取,并且总是在更新时写回主内存。
竞争条件
如果两个或多个线程共享一个对象,并且多个线程更新该共享对象中的变量,则可能发生竞争条件。
如果线程A将一个共享对象的变量count读入它的CPU缓存中。再想象一下,线程B做同样的事情,但是放到了不同的CPU缓存中。现在线程A将1添加到计数中,线程B也做同样的事情。现在var1已经增加了两次,一次在每个CPU缓存中。如果这些增量顺序执行,变量count将增加两次,并将原始值+ 2写回主存。
但是,这两个增量是同时执行的,没有进行适当的同步。不管线程A和线程B将更新后的count写回主存,更新后的值将只比原始值高1,尽管有两次增量。
要解决这个问题,可以使用Java synchronized block。synchronized保证在任何给定时间只有一个线程可以进入代码的给定临界区。synchronized还保证在同步块中访问的所有变量都将从主内存中读取,当线程退出同步块时,所有更新的变量将再次刷新到主内存中,无论该变量是否声明为volatile。
内存交互
一个变量如何从主内存拷贝到工作内存,从工作内存同步回主内存的实现细节 JMM定义了以下8种操作来完成,都具备原子性:
- lock(锁定) 作用于主内存变量,把一个变量标识为一条线程独占的状态
- unlock(解锁) 作用于主内存变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定 unlock之前必须将变量值同步回主内存
- read(读取) 作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后的load
- load(载入) 作用于工作内存变量,把read从主内存中得到的变量值放入工作内存的变量副本
- use(使用) 作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值得字节码指令时将会执行这个操作
- assign(赋值) 作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
- store(存储) 作用于工作内存变量,把工作内存中一个变量的值传送到主内存,以便随后的write操作使用
- write(写入) 作用于主内存变量,把store操作从工作内存中得到的值放入主内存的变量中
这里的lock和unlock是实现synchronized的基础,Java并没有把lock和unlock操作直接开放给用户使用,但是却提供了两个更高层次的指令来隐式地使用这两个操作,即moniterenter和moniterexit