Java内存结构

概述

Java内存模型是JVM与计算机内存协同工作的规范与准则。Java虚拟机中的线程与内存结构和计算机的CPU与主存的架构并不相同,因此才需要一套规则来整合两种模型来协同工作。

这套规则描述了线程间共享变量的可见性和多线程访问带来的冲突等问题。

原始的Java内存模型天生存在一些不足之处,因此在JDK1.5之后做了重新修订。

多线程下的Java内存结构

我们已经了解了Java的内存结构主要被分为线程栈和堆。线程栈中存放的变量只在该线程内部可见,且各线程间均只可见自己线程栈中的变量。堆中存放的都是线程间共享的变量,对所有线程均可见。

额外需要说明的是,如果线程中的变量是引用类型的,则被引用的对象是被分配在堆上的。同样地,对象所有的成员变量,包括静态的成员变量,都是被分配在堆上的。

硬件内存架构

硬件的内存架构与JVM的内存结构从设计上就是不同的。

计算机的核心是CPU,通常一台计算机都会有多个CPU,那么多线程情况下,通过多个CPU的同时执行,线程将会出现并发的情况。

计算机的运行时存储单元主要是主存,或者称为内存。主存与CPU之间的数据交互会通过多级缓存和CPU寄存器。在这些存储单元中的数据对CPU来说都是共享的。

CPU执行任务时的典型数据流向通常是这样的:从主存读取部分数据到多级缓存中,再从多级缓存把数据写入CPU寄存器中,CPU从寄存器中取出数据进行计算,并将计算结果写入寄存器,寄存器再把数据刷回多级缓存中,多级缓存再把数据刷回主存。

Java内存结构与计算机硬件架构的协同工作

两种架构的差异主要在于线程栈和堆在硬件架构中是混杂在主存、多级缓存和寄存器中的。

接下来我们来看看这种架构上的差异会导致什么问题。

可见性

可见性问题的产生主要由多级缓存以及寄存器的存在引起。

通常,CPU在读取数据时,会逐层检查数据是否存在在寄存器、多级缓存、主存中,如果存在,会直接获取。而CPU写入数据时,则只会把数据写入到寄存器中。寄存器到多级缓存以及主存的刷回是由操作系统来执行的,执行的频率不可预知。

在多线程任务执行时,多个线程由多个CPU并发执行,数据的交互可能仅与各CPU的寄存器有关,此时就会出现可见性问题。

举个例子,count++这样的代码在多线程执行时,由于可见性问题,会出现得到的结果比预期结果小的情况。这样的结果原因如下,一个线程从主存中取count的值时,另一个线程加一的结果还没有写回主存,这时,当前线程获取到的count原始值是比预期值小于1的。

解决上述问题最合适的方案是使用volatile关键字。在Java 1.5以后,volatile关键字会保证用该关键字修饰的变量在写入时都会刷新到主存中,以保证任何时刻任何线程从主存中获取到的值都是正确的。

条件竞争(Race Condition)

条件竞争是指多线程对某个资源的访问没有被正确同步,没有被正确同步的概念通常指多线程下对同一个资源的读写存在竞争,即读写的顺序没有在逻辑上被指定。

同样举例count++这句代码,在多线程执行时,由于没有指定读写的顺序(这里的加一操作不是原子操作,编译后会产生读取和写入两个操作),我们考虑线程1读,线程2读,线程1写,线程2写这样的顺序。由于线程1和2都读到的是count的原始值,两个线程分别加一之后写入,得到的仅仅是加1之后的结果,而预期是加2之后的结果。这就是典型的条件竞争问题。

解决条件竞争最基本的方案是使用synchronized关键字来修饰需要同步的代码块,该关键字能保证同一时刻只能有一个线程能执行该代码块中的操作,并且退出代码块时,所有被更新的变量都会刷新回主存。这样就能保证没有其他线程的代码会插在被同步的代码之间执行了。