Java内存模型

Java内存模型规定了Java虚拟机如何与计算机内存一起工作。JVM内存模型即Java内存模型。

开发正确的并发程序,理解Java内存模型非常重要。Java内存模型规定了线程间如何操作共享变量的值,以及如何同步访问共享变量。

Java内存模型一开始并不完善,Java 1.5中修复了早期内存模型缺陷,这版内存模型Java 8依然在使用。

线程栈

JVM中每个线程都有自己的线程栈,线程栈包含有关线程调用了哪些方法,以及到达的调用点信息。通常称其为“调用堆栈”。当线程执行代码时,调用栈会发生变化。

线程栈还包含栈上所有方法的局部变量。所有基本类型的局部变量(boolean,byte,short,char,int,long, float,double)都存储在线程栈上,一个线程可以将一个原始类型变量的副本,传递给另一个线程,但它们之间并不共享同一个变量。

堆内存

堆内存包含Java应用创建的所有对象,无论是为局部变量创建的对象,还是专为另一个对象的成员变量所创建,对象都存储在堆中。其中包括原始类型的对象(如:Byte、Integer、Long等)。

内存架构

现代计算机都会有多个CPU,其中一些CPU还可能具有多个内核。在具有多个CPU的现代计算机上,能同时运行多个线程。

每个CPU包含一组寄存器,寄存器本质上是CPU内存。每个CPU都有一个高速缓存。CPU访问高速缓存比访问主内存快,但最快的还是访问内部寄存器。某些CPU可能会有多级高速缓存。

所有CPU都能访问主内存。通常,当CPU需要访问主内存时,将部分主内存读入CPU缓存。甚至可以将缓存的一部分读入内部寄存器,然后,对其执行操作。当CPU需要将结果写回主内存器时,会将值从内部寄存器刷新到高速缓存,并在某个时刻将值刷回主内存。

当CPU需要在高速缓存中存储其他内容时,会将存储在高速缓存中的值刷回主内存。CPU缓存可以一次只写入一部分,不必每次更新时都读取/写入完整的缓存。通常以“缓存行”为单位进行更新。可以将一个或多个高速缓存行读入高速缓存,也可以将一个或多个高速缓存行再次刷新回主内存。

差异

综合上述可以看出,Java内存模型和硬件内存体系结构之间存在差异。

硬件内存体系结构中不分线程栈和堆。在硬件上,线程栈和堆都位于主内存中。有时,部分线程栈和堆可能会出现在CPU缓存和CPU寄存器中。

当对象和变量能够存储在计算机各个不同存储区域时,可能会出现某些问题:

线程更新/写入到共享变量的可见性。

读取、检查和写入共享变量时产生的竞争。

可见性

如果多个线程共享一个对象,而没有正确使用同步或volatile声明,则一个线程对共享对象所做的更新可能对其他线程不可见。

共享对象最初存储在主内存中,在CPU 1上运行的线程,将共享对象读入CPU缓存,对共享对象作了修改。只要没有将CPU缓存刷回主内存,在其他CPU上运行的线程,就不会看到CPU 1对共享对象所做的修改。如此,每个线程都拥有自己的共享对象副本,每个副本位于不同的CPU缓存中。

Java使用volatile关键字,确保直接从主内存读取指定的变量,并在更新时立即将其写回主内存。

竞争

如果多个线程共享一个对象,且有多个线程同时更新该共享对象的变量,则可能发生竞争。

如果线程A将一个共享对象的count变量读入CPU缓存。线程B也做了相同的操作,两个线程位于不同的CPU。现在线程A将count加1,线程B也执行同样的操作。对变量count所做的两次加1操作,都存在各个CPU的缓存中。

在顺序执行的条件下,变量count的两次+1操作,最终以将原始值+2的结果写回到主内存。

在没有同步的条件下同时执行,更新后的值只会比原始值多1。

Java使用同步块解决竞争问题,确保在同一时刻只有一个线程,能够进入指定的临界区。Synchronized块还保证在Synchronized块中访问的所有变量,都将从主内存中读入,当线程退出Synchronized块时,所有更新的变量都将被刷回主内存,而不管变量是否声明了volatile。