http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
概述
The Java memory model specifies how the Java virtual machine works with the computer's memory (RAM).
- 原始的Java内存模型不足,所以Java内存模型在Java 1.5中进行了修改。这个版本的Java内存模型仍然在Java 8中使用。
The Internal Java Memory Model
- The Java memory model used internally in the JVM divides memory between thread stacks and the heap. (JVM内部使用的Java内存模型将线程堆栈和堆之间的内存分割。)
该图从逻辑角度说明了Java内存模型:
Java虚拟机中运行的每个线程都有自己的线程堆栈。线程堆栈包含有关线程调用哪些方法来达到当前执行点的信息。我将其称为“调用堆栈”。
当线程执行其代码时,调用堆栈发生更改。线程堆栈还包含执行的每个方法的所有局部变量(调用堆栈中的所有方法)。
一个线程只能访问它自己的线程堆栈。线程所创建的局部变量对于所有其他线程,不同于创建线程的线程。即使两个线程执行完全相同的代码,两个线程仍然会在每个自己的线程堆栈中创建该代码的局部变量。因此,每个线程都有自己的每个局部变量的版本。
基本类型(所有局部变量 boolean,byte,short,char,int、long, float,double)完全存储在线程栈上,因此不是其他线程可见。
一个线程可以将一个pritimive变量的副本传递给另一个线程,但是它不能共享原始局部变量本身。
堆包含您的Java应用程序中创建的所有对象,无论创建对象的线程如何。
这包括原语类型(例如对象的版本Byte,Integer,Long等等)。如果对象被创建并分配给局部变量,或者创建为另一个对象的成员变量,则对象仍然存储在堆上,这并不重要。
这是一个图示出了存储在线程堆栈上的调用栈和本地变量以及存储在堆上的对象:
以下是上图所示的图:
Hardware Memory Architecture
现代硬件内存架构与内部Java内存模型有所不同。了解硬件内存架构也很重要,以了解Java内存模型的工作原理。本节介绍常见的硬件内存架构,后面的部分将介绍Java内存模型的工作原理。
以下是现代计算机硬件架构的简化图:
现代计算机通常有2个或更多的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缓存一次可以将数据写入其内存的一部分,并一次刷新其内存的一部分。
每次更新时,它不必读取/写入完整的缓存。
通常,缓存在被称为“高速缓存行”的更小的存储块中被更新。
可以将一个或多个高速缓存行读入高速缓冲存储器,并且可以将一个或多个高速缓存线重新刷回主存储器。
Bridging The Gap Between The Java Memory Model And The Hardware Memory Architecture
- 如前所述,Java内存模型和硬件内存架构是不同的。硬件内存架构不区分线程堆栈和堆。在硬件上,线程堆栈和堆都位于主内存中。线程堆栈和堆的一部分有时可能存在于CPU高速缓存和内部CPU寄存器中。
这在图中说明:
- 当对象和变量可以存储在计算机的各种不同的存储区域中时,可能会出现某些问题。两个主要问题是:
线程更新(写入)到共享变量的可见性。
阅读,检查和写入共享变量时的竞争条件。
这两个问题将在以下部分中解释。
Visibility of Shared Objects
如果两个或多个线程共享一个对象,没有正确使用volatile声明或同步,一个线程所做的共享对象的更新对其他线程可能不可见。
假设共享对象最初存储在主内存中。在CPU上运行的线程然后将共享对象读入其CPU缓存。在那里它对共享对象进行了更改。
只要CPU缓存没有被刷新到主内存,共享对象的更改版本对于在其他CPU上运行的线程是不可见的。
这样一来,每个线程可能最终都有自己的共享对象副本,每个副本都坐在不同的CPU缓存中。
下图说明了草图的情况。
在左CPU上运行的一个线程将共享对象复制到其CPU缓存中,并将其
count变量更改为2.对于在右侧CPU上运行的其他线程,此更改不可见,因为更新count尚未刷新到主内存。
Race Conditions
如果两个或多个线程共享对象,并且多个线程更新该共享对象中的变量,则 可能会发生竞争条件。
假设线程A将count共享对象的变量读入其CPU缓存中。想象一下,线程B执行相同的操作,但是进入不同的CPU缓存。
现在线程A添加一个count,线程B也一样。现在var1已经增加了两次,每次CPU缓存一次。如果这些增量依次执行,则变量count
将被递增两次,并将原始值+ 2写回主存储器。但是,两个增量是在没有正确同步的情况下同时进行的。不管线程A和B中哪一个将其更新版本写
count回到主内存,尽管有两个增量,但更新后的值将仅比原始值高1。
该图示出了如上所述的竞争条件的问题的发生:
- 同步块保证在任何给定时间只有一个线程可以进入代码的给定关键部分。
- 同步块还保证在同步块内访问的所有变量将从主存储器读入,并且当线程退出同步块时,所有更新的变量将被再次刷新回主存储器,而不管该变量是否被声明为volatile或不。