Java Memory Model

今天看《Java多线程实战指南-设计模式篇》的时候发现里面提到一个 Java 内存模型(JMM,Java Memory Model)术语,对这个概念有些模糊了,就在网上查找资料。

找到一篇比较好的文章

Java Memory Model

发现已经有好几篇中文翻译了,不过还是想自己动手试一试


主要内容:

  1. 引言
  2. Java 内存模型(The Internal Java Memory Model
  3. 硬件存储器体系结构(Hardware Memory Architecture
  4. 解决 Java 内存模型与硬件内存结构之间的差异

引言

Java 内存模型指定了 Java 虚拟机(JVM,Java Virtual Machine)如何与计算机内存(RAM)一起工作。Java 虚拟机是一个计算机模型(a model of a whole computer),所以它包含了一个内存模型,又名 Java 内存模型。

想要设计好的并发程序(design correctly behaving concurrent programs)必须了解 Java 内存模型,它指定了线程如何查看和写入共享变量,以及如何同步访问共享变量(how and when different threads can see values written to shared variabels by other threads, and how to synchronize access to shared variables when necessary

原始的 Java 内存模型存在不足之处,在 Java1.5 版本进行过修改,新版本的 Java 内存模型在 Java8 同样适用


Java 内存模型

JVM 中,Java 内存模型将内存分为线程栈(Thread Stack)和堆(Heap)。其逻辑图如下

这里写图片描述

扫描二维码关注公众号,回复: 2973158 查看本文章

JVM 中每个线程均拥有各自的线程栈,线程栈中保存有该线程执行到指定位置所调用的方法,称之为调用栈(call stack

线程栈中还保存了调用栈方法的局部变量(local variable),由于线程仅能访问自己的线程栈,所以其它线程无法访问当前线程所创建的局部变量。即使两个线程正在执行同一段代码,它们会在各自的线程栈中创建自己的局部变量

原始类型(primitive type,包括 boolean,byte,short,char,int,long,float,double)的局部变量完全保存在线程栈中,无法被其它线程所访问,当前线程只可以将该局部变量的值复制给其它线程。

堆保存了 Java 应用程序中创建的所有对象,无论是哪个线程创建的对象,均放置在堆中。比如原始类型的包装类(Byte,Integer,Long等);或者是在方法中创建的局部对象;以及对象中包含的其它对象。

下图展示了线程栈中保存了调用栈以及局部变量,堆中保存了对象

这里写图片描述

如果局部变量是原始类型,那么它保存在线程栈中;如果局部变量是一个对象引用(a reference to an object),那么该引用保存在线程栈中,对象保存在堆中。

对象的成员变量(member variable)和对象一样保存在堆中,不论是原始类型,还是对象的引用(That is true both when the member variable is of a primitive type, and if it is a reference to an object)。

静态类变量和类定义一起存储在堆中(Static class variables are also stored on the heap along with the class definition

只要线程拥有对象的引用,它可以访问堆中的所有对象,那么它也可以访问对象的成员变量。如果两个线程在同一时间(at the same time)调用了同一对象(on the same object)的方法,它们都可以访问对象的成员变量,但是每个线程拥有各自独立的局部变量。

上述说明如下所示

这里写图片描述

在上图中,两个线程均拥有一组变量。其中一个变量(Local variable 2)指向了堆中的一个共享对象(a shared objectObject3。这个引用是一个局部变量,所以保存在线程各自的线程栈中

共享对象 Object3 有两个成员变量是对象 - Object2Object4,所以线程也可以通过 Object3 访问 Object2Obejct4

两个线程栈中的一个局部变量(Local variable 3)也引用了一个对象,不过是独立的,分别引用了堆中的 Object1Object5

其实现代码示例如下:

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 member1 = 67890;
}

当两个线程执行 run 方法后,JVM 内存执行如上图所示。run 方法调用了 methodOne,其后 methodOne 调用了 methodTwo

methodOne 定义了一个原始类型局部变量 localVariable1 和一个引用对象的局部变量 localVariable2

每个线程执行 methodOne 后都将在线程栈中创建局部变量 localVariable1localVariable2,其中,localVariable2 均指向堆中的同一个对象。上面代码中,localVariable2 指向的是一个静态变量引用的对象。静态变量保存在堆中,仅创建一个,所以两个变量 localVariable2 指向的是同一个类 MySharedObject 实例。实例 MySharedObject 也存储在堆中,对应上图中的 Object3

MySharedObject 包含了两个成员变量,它们和对象一起保存在堆中,这两个成员变量指向了两个 Integer 类型对象,它们对应上图中的 Object2Object4

每次执行 methodTwo 时,都会创建一个局部变量 localVariable1,以及创建一个类实例 Integer,所以局部变量 localVariable1 保存在线程栈中,类实例 Integer 保存在堆中,且每个线程中 localVariable1 引用的类对象相互独立,分别对应上图中的 Object1Object5

上图中类 MySharedObject 还拥有了两个原始类型成员变量 member1member2,它们和类对象一起保存在堆中


硬件存储器体系结构

现代硬件存储器体系结构(Modern Hardware Memory Archiecture)和 Java 内存模型有些不同,本小节先描述一个通用的硬件存储器体系结构,然后下一小节讨论 Java 内存模型如何适配硬件存储器体系结构。

下面是一个现代硬件存储器体系结构的简化图

这里写图片描述

现在计算机都有 2 个或者更多的 CPU,同时每个 CPU 也可能有多个核心。单个 CPU 在一段时间内可以运行单个线程,那么多个 CPU 的出现让多线程同时运行成为了可能

每个 CPU 都有一组寄存器,大多数情况下还拥有缓存,所有 CPU 共享一个主内存

3 种存储介质的大小如下:

主内存 > 缓存 > 寄存器

CPU 访问这 3 种存储介质的速度如下:

寄存器 > 缓存 > 主内存

通常情况下,CPU 需要读取主内存数据时,它会将部分主内存数据读取到缓存,甚至将部分缓存数据读取到寄存器,再进行操作;而当 CPU 将数据写入主内存时,首先它将寄存器数据刷新到缓存,然后在某一时刻将缓存数据写入主内存

缓存数据写入主内存的时机通常是在 CPU 需要将数据写入缓存时,缓存会将部分数据写入主内存,然后接收 CPU 写入的数据,不会一次性将所有缓存数据写入主内存中。通常情况下,每次缓存操作(读写)都以一个很小的内存块为单位,称之为 cache lines。缓存读时,会将一个或多个 cache lines 写入主内存;同样的,缓存写时,CPU 会将一个或多个 cache lines 写入缓存

额外问题1:为什么存储介质区分为寄存器,缓存和内存

总的结论:速度和价格的平衡。现代计算机存储结构

CPU 寄存器 - 缓存 - 内部存储器(内存)- 外部存储器(硬盘)

就寄存器,缓存和内存而言,寄存器的读写速度最快,内存的读写速度最慢,缓存介于两者之间

有几个原因:

  • 介质不同。缓存使用 SRAM,内存使用 DRAM
  • 距离不同。距离 CPU 越近,读取时间越短,寄存器就在 CPU 里面,缓存次之,内存最远;
  • 设计方式不同。寄存器小,可以设计成高耗电和高成本,而内存不行;
  • 工作方式不同。寄存器的读取方式相对简单;内存读取方法复杂。

参考:

为什么寄存器比内存快?

请问CPU,内核,寄存器,缓存,RAM,ROM的作用和他们之间的联系?

额外问题2:为什么缓存层中有多级缓存

通常情况下,查询数据时会在同一片存储区域

程序运行情况表明,程序产生的地址往往集中在存储器逻辑地址空间的很小范围内
而指令通常又是顺序执行,所以分布连续,再加上循环程序和子程序段都要运行多次
数据也是如此,这种现象称为“程序访问的局部性Locality of Reference”

所以设计缓存层,存储容量小于内存,但是可以将常用的数据存放在里面,并且由于缓存容量小,可以设计成更昂贵,更耗电,以加快查询速度

而且 L1 Cache 的大小受限于硅的局限性以及高时钟频率的要求,所以设计 L2 Cache,来弥补 L1 Cache 缺失的数据

参考:

为什么在 CPU 中要用 Cache 从内存中快速提取数据?

多级Cache原理


解决 Java 内存模型与硬件内存结构之间的差异

上两节提到的 Java 内存模型和硬件内存结构存在差异性,同时硬件内存结构无法区分线程栈和堆

在硬件内存结构中,线程堆和栈可能出现在寄存器,缓存和主内存中。如下图所示

这里写图片描述

当对象和变量可能存储在硬件存储器的不同位置时,会出现一些问题。主要有以下两个方面:

  • 线程写入共享变量时的可见性(Visibility of thread updates (writes) to shared variables
  • 读取,检测和写入共享变量时的竟态条件(Race conditions when reading, checking and writing shared variables

下面解决这两个问题

共享对象的可见性(Visibility of Shared Objects

两个线程共享同一个对象

  • 起初,该对象保存在主内存中;
  • 其中一个线程进入运行状态,读取该对象到 CPU 缓存,并进行相关操作;
  • 操作完成后,该 CPU 并没有将缓存中的对象刷新到主内存,此时,修改后的对象对于在其它 CPU 运行的线程来说不可见;
  • 另一个 CPU 上的线程读取主内存中的对象到缓存,此时,主内存中的对象状态落后于先前缓存中的对象状态。

下图演示了这种情况:

  • 运行在左边 CPU 的线程将主内存中的共享共享对象写入该 CPU 缓存,并改变其变量 count2,但是该对象变化没有及时刷新到主内存;
  • 当运行在右边 CPU 的线程从主内存读取共享对象到 CPU 缓存时,其 count 变量值仍为 1

这里写图片描述

为解决这一问题,可用 volatile 关键字修改该变量。它确保了每次读取和写入变量操作都是从主内存进行,保证了数据的可见性

竟态条件(Race Conditions

当多个线程同时对一个共享变量进行更新时,将会发生竟态条件

假如线程 A 读取了共享对象的变量 countCPU 缓存,同时线程 B 也读取了变量 count,此时两个线程在不同的 CPU 中运行

然后两个线程均对变量 count 执行 +1 操作,如果这两个对象依次将结果刷新会主内存,此时 count 值比原先大 1,但是正确结果应该是大 2,因为执行了两次 +1 操作

下图证明了这种情况

这里写图片描述

为解决竟态条件,可以使用同步块 (synchronized block),同步块保证了每次仅有一个线程执行临界区域的代码

同时,同步块中的变量均从主内存进行读取,当前线程运行完同步块代码后,将变量数据重新刷新到主内存,不论该变量是否有 volatile 关键字定义

猜你喜欢

转载自blog.csdn.net/u012005313/article/details/81103911