前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java内存模型

Java内存模型

作者头像
码代码的陈同学
发布2018-07-25 00:04:14
9340
发布2018-07-25 00:04:14
举报
文章被收录于专栏:码代码的陈同学

原文:Java Memory Model by Jakob Jenkov on 2014-12-18 翻译:陈同学, 注:原文撰写于14年,部分小知识点描述已不准确。

Java内存模型(简称JMM)指定了JVM如何利用计算机内存(RAM)进行工作。JMM与整个计算机的模型类似,这个模型自然也包含内存模型,即Java内存模型(AKA)。

如果你想设计出良好的并发程序,理解JMM十分重要。JMM定义了不同线程 何时 以及 如何 看到其它线程写入的共享变量值,以及在有必要时如何以同步的方式访问共享变量。

由于最初的JMM无法胜任工作,因此在Java 1.5中对JMM进行了升级,该版本在Java 8中依然在使用。

JMM技术内幕

JVM中的JMM将内存划分为 线程栈(Thread Stack)堆(Heap),下图从逻辑上展示了JMM。

JVM中运行的每个线程都拥有自己的线程栈,它存储了线程调用过程中的所有方法以及当前执行点等信息,我更乐意将线程栈称为 调用栈(Call Stack)。调用栈会随着代码的不断执行而不断改变。

线程栈也包含了每个方法被执行时所需的局部变量(所有方法都在调用栈中)。一个线程仅能访问自己的线程栈,它所创建的局部变量对其他线程都是不可见的。即使两个线程执行完全相同的代码,它们也会在自己的线程栈中创建对应的局部变量。

所有基础数据类型(boolean,byte,short,char,int,long,float,double) 的局部变量全部存储在线程栈中,线程之间相互不可见。一个线程可以将基础类型变量的副本(copy)传递给其他线程,但是无法在线程之间共享这种变量。

堆中存储了Java应用中所有线程创建的对象,包含了基础数据类型的包装类(如:Byte,Integer,Long等)。对象创建后无论是赋值给局部变量,亦或是作为某对象的成员变量,它都将存储在堆中。

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

  • 若局部变量是基础数据类型,将存储于线程栈
  • 若局部变量是一个指向其他对象的引用,引用值存储在线程栈中,引用的对象存储在堆中
  • 若一个对象的方法中包含局部变量,尽管对象存储在堆中,但方法执行时的局部变量将存储在线程栈中
  • 对象的成员变量将与对象本身一起存储在堆中,无论成员变量是基础类型还是引用类型。
  • 类的静态变量与类的定义信息一起存储在堆中
  • 线程之间通过引用的方式共享堆中的对象。当一个线程访问一个对象时,它也可以访问其成员变量。如果两个线程执行某对象的同一个方法时,两个线程都可以访问对象的成员变量,但每个线程将拷贝一份方法所需的局部变量到自己的线程栈。

下图演示了上述知识点。

两个线程都有一系列局部变量。两个线程的 Local Variable 2 都指向堆中的共享对象 Object3,两个引用分别存储在每个线程的线程栈中。

注意,Object3以成员变量的形式拥有Object2和Object4的引用。两个线程可以通过Object3的成员变量访问到Object2和Object4。

那么,什么样的Java代码可以形成上图的情景呢?下面用简单的代码展示一下。

代码语言:txt
复制
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.
    }
}
代码语言:txt
复制
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;
}

译者注:由于图+代码已非常浅显易懂,笔者省略了作者对于上述代码的大段描述。

硬件内存结构

当代硬件内存结构与JVM内部的内存模型稍有不同。为了理解JMM如何与其打交道,知晓硬件内存结构十分重要。本部分描述了通用硬件内存结构,后续将讲述JMM如何与之协同工作。

下面是一张当代计算机硬件结构的简图:

现在的计算机经常有2个或多个CPU,一些CPU可能有多核。这意味着,多CPU计算机可以同时运行多个线程,每个CPU在任何给定的时间内都有能力运行一个线程。那么,如果Java应用是多线程的,每个CPU都可以并发的运行一个线程。

每个CPU包含一系列寄存器,CPU在寄存器上执行操作的速度远远超过操作主内存(简称主存)中变量的速度,这是因为CPU访问寄存器的速度比访问主存的速度快很多。

每个CPU也可能包含CPU cache层。实际上,现在大多数CPU都有一定大小的cache,CPU访问cache的速度远超访问主存的速度,当然肯定比不上访问寄存器的速度。因此,CPU cache的访问速度介于寄存器与主存之间。有的CPU可能有多级cache(Level1 和 Level2),但这对于理解JMM如何与内存交互并不重要,只需要知道CPU有cache层即可。

计算机包含一块主内存(RAM)。所有CPU都可以访问主存,主存的大小比CPU的cache大很多。

通常,当CPU需要访问主存时,它会先从主存中读取一部分数据到CPU cache,同样也可能读取部分CPU cache到寄存器再进行操作。当CPU需要回写结果到主存时,首先会将数据从寄存器flush到CPU cache,然后在某个时间点再将数据flush到主存。

一般当CPU cache需要存储其他数据时,会将cache中存储的数据flush到主存。CPU cache可以每次向主存回写一部分数据,并且刷新cache中的部分数据,并不需要每次读/写cache的所有数据。cache通常以更小的内存块—— cache line 为单位进行更新,每次可以将一个或多个 cache line读入CPU cache,每次也可以将一个或多个cache line flush到主存。

连接JMM和硬件内存

上面已提到,JMM和硬件内存存在差异。硬件内存并不区分堆和线程栈,在硬件上,堆和线程栈都在主存中,部分线程栈和堆内存可能在CPU cache或寄存器中。如下图所示:

由于对象和变量可以存储在计算机的不同内存区域,可能会出现某些问题。两个主要问题是:

  • 线程更新共享变量时的可见性问题
  • reading、checking、writting 共享变量时的竞争条件(Race Condition)问题

下面聊聊这两个问题。

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

如果两个或更多线程共享一个对象,若没有使用 volatile 进行修饰或使用同步机制,一个线程更新该共享对象后对其他线程将可能不可见。

想象一下,一个共享对象初始化后存储在主存中。运行在CPU1上的一个线程读取了该对象到自己的CPU cache中,然后对对象做了变更,由于CPU1 cache还没flush回主存,运行在其他CPU上的线程将看不到变更后的对象。这样的话,每个线程都可以拥有一个共享对象的副本,每个副本都位于不同CPU的 cache中。

下图演示了这种场景。一个运行在左边CPU的线程拷贝了一份共享对象并存储到自己的CPU cache,然后将对象的成员变量count 从1变更为2,这个变更对于运行在右边CPU上的线程来说并不可见,因为变更后的 count 还没flush回主存。

为了解决这个问题,你可以使用 Java volatile 关键字。volatile关键字可以确保一个变量总是从主存直接读取,而且在更新后总是会将新的变量值flush回主存。

竞争条件(Race Condition)

如果多个线程共享一个对象,当多个线程都更新了该对象中的变量时,就会发生 Race Condition 问题。

想象一下,如果线程A读取了共享对象的 count 属性到自己的CPU cache中,然后线程B也将其读取到了自己的CPU cache中。现在,线程A将count的值加1,线程B也一样加1,变量在每个CPU中都进行了加1操作。

如果这些操作顺序执行,变量count将执行2次加1操作,而且会将count + 2的值回写到主存。

然而,如果两次加1的操作并发执行(也未使用同步),无论是线程A还是线程B将更新后的 count 写回主存,count 的值只会被加1,而不是被加2。

下图展示了上述过程:

你可以使用 Java synchronized block(同步块) 来解决这个问题。同步块保证每次只有一个线程可以进入临界区执行代码,它同时保证了无论变量是否使用 volatile 修饰,同步块中的变量都会从主存中读取,并在退出同步块时将更新后的变量flush回主存。


欢迎关注陈同学的公众号,一起学习,一起成长

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JMM技术内幕
  • 硬件内存结构
  • 连接JMM和硬件内存
    • 共享对象的可见性(Visibility of Shared Objects)
      • 竞争条件(Race Condition)
      相关产品与服务
      对象存储
      对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档