java内存模型包括什么(一篇文章彻底搞懂)

一、Java内存模型定义

Java作为一个跨平台的语言,它的实现要面对不同的底层硬件系统,设计一个中间层模型来屏蔽底层的硬件差异,给上层的开发者一个一致的使用接口。Java内存模型就是这样一个中间层的模型,它为程序员屏蔽了底层的硬件实现细节,支持大部分的主流硬件平台。

Java内存模型(Java Memory Mode):Java虚拟机内存如何与计算机内存(RAM)一起工作。Java虚拟机是是整个计算机的模型,所以这个模型自然包含一个内存模型,也可以说JMM是Java虚拟机内存使用规范。

Java内存模型规定了不同线程如何以及何时可以看到其他线程写入共享变量的值,以及如何在必要时同步对共享变量的访问。

注意:Java Memory Model并不是真实存在的,它只是物理内存模型的一个映射。

JVM中Java内存模型分为线程栈(Thread Stack)和堆(Heap),下图从逻辑角度描绘了Java内存模型。

java内存模型包括什么(一篇文章彻底搞懂)(1)

1. 线程栈 Thread Stack

运行在JVM中的每一个线程都有它自己的线程栈(Thread Stack),线程栈包含了线程调用的方法,以及当前执行点的信息。线程栈也包含每一个被执行方法的所有局部变量,每一个线程只能方法它自己线程栈,线程创建的局部变量对其他线程而言是不可见的。即使两个线程执行完全相同的代码,两个线程仍然在自己的线程栈中创建局部变量。这样,每一个线程都有其自身版本的局部变量。

2. 堆 Heap

堆(Heap)中包含了Java应用中创建的所有对象,不管是哪一个线程创建的对象。其中包括简单类型的对象版本,如Byte、Integer、Long等等。不管创建的对象是分配给局部变量的,还是另外一个对象的成员变量,这一对象都存储在堆中。


下图描绘了存储在线程栈的调用栈(call stack)和局部变量,以及存储在堆中的对象。

java内存模型包括什么(一篇文章彻底搞懂)(2)

3. 存储方式

如果一个局部变量是简单类型,那么它将存储在线程栈中。

如果一个局部变量是一个对象的引用,这种情况下,对象引用(reference,局部变量)存储在线程栈中,但是对象自身存储在堆中。

一个对象可能包含一些方法,且这些方法又包含了局部变量。这些局部变量也存储在线程栈中,即使这些方法所在的对象存储在堆中。

一个对象的成员变量随着对象自身存储在堆中,无论该对象类型是引用类型(reference)或者是基本类型(primitive type)。 静态变量和对象类定义存储于堆上。


具有该对象引用的所有线程都可以访问堆中的对象。当一个线程访问该对象时,也可以访问该对象的成员变量。如果两个线程在同一时间调用相同对象的同一个方法,它们都可以访问对象的成员变量,但是每一个线程都有它自己的局部变量副本。

下面这张图描述了上述这些要点:

java内存模型包括什么(一篇文章彻底搞懂)(3)

二、硬件内存架构 Hardware Memory Architecture

现代硬件内存架构和内部的Java内存模型有点不同。为了理解Java内存模型是如何使用它的,理解硬件内存架构也很重要。

这里,我们将介绍通用的硬件内存模型,随后将描述Java内存模型是如何使用它的。

如下是关于现代计算机硬件模型的简化图像:

java内存模型包括什么(一篇文章彻底搞懂)(4)

一台现代计算机经常有2个或更多CPU,并且这些CPU 一般有多核。也就是说。在现代计算机中有2个或多个CPU,这样就会多个线程同时运行。每个CPU在任一给定时间可以运行一个线程。这意味着,如果我们的Java应用是多线程的,一个CPU运行一个线程,这样Java应用就并发运行了。

每个CPU包含一组寄存器,这些寄存器本质上是在CPU中的内存。CPU 在这些寄存器中执行操作比在主内存(Main memory)中更快。这是因为CPU访问这些寄存器比访问主内存更快。

每个CPU也有CPU缓存层(Cache memory layer)。CPU访问缓存也比访问主内存快,但是通常没有访问它内部的寄存器快。因此,CPU缓存访问速度是介于它内部寄存器和主内存之间。一些CPU可能有多层缓存(Level 1和Level 2)。

计算机也通常有主内存(RAM),所有的CPU能够访问主内存,主内存容量通常比CPU缓存大很多。

一般情况下,当CPU需要访问主内存时,它将读取部分主内存到CPU缓存,甚至也会读取部分缓存到它内部的寄存器,接着执行操作。当CPU 需要将结果写回主内存时,它会将值从内部寄存器刷新到缓存, 并在某个时候将值刷新回主内存。



三、Java内存模型和硬件内存架构映射关系

如前面提到的,Java内存模型和硬件内存架构是不同的。硬件内存架构不区分线程栈(thread stack)和堆(heap)。在硬件内存架构中,线程栈和堆都存储在主内存中。部分线程栈和堆有时也存储在CPU缓存和内部寄存器中,如下图所示:

java内存模型包括什么(一篇文章彻底搞懂)(5)

当对象和变量存储在计算机内不同的内存区域时,可能会产生一些问题,主要的2个问题是:

  • 共享变量的可见性(Visibility of Shared Objects);
  • 当读取、检查和更新共享变量时出现Race Condition(竞争条件);

接下来对这两个问题进行解释。


1. 共享变量的可见性(Visibility of Shared Objects)

可见性:一个线程对共享变量的修改,能够及时地到主内存并且让其他的线程看到。

如果2个或更多线程共享同一个对象时,没有合适地使用volatile 声明或synchronized 关键字,一个线程对共享变量的更新可能另外的线程不可见。

线程对共享变量的修改,只能在自己的工作内存里操作,不能直接对主内存中的共享变量进行修改,而且一个线程不能直接访问另一个线程中的变量的值,只能通过主内存进行共享传递。


想象一下,起初共享变量存储在主内存中,CPU 上运行的一个线程读取了共享变量的值到CPU缓存,接着对共享变量进行了更新操作。只要当CPU缓存没有刷新回主内存,共享变量的更新值对运行在其他CPU上的线程是不可见的。这样,每个线程都拥有它自己的共享变量的副本,并且存储在不同的CPU缓存中。

下图描绘了这个过程。线程1运行在左边的CPU上,复制了共享变量到它的CPU缓存中,更新count=2。这个更新操作对运行在右边CPU上的线程是不可见的,因为对count的更新操作并没有刷新回主内存中。

java内存模型包括什么(一篇文章彻底搞懂)(6)

为了解决这个问题,可以参考小编之前的文章《Java面试题:Java多线程之内存可见性》,可以使用Java内置的volatile关键字。volatile 关键字可以确保给定的变量直接从主内存读取,并且当更新之后,总是写回主内存。


2. 竞争条件 Race Conditions

如果2个或更多的线程共享一个对象,并且多个线程更新这个共享对象中的变量,竞争条件Race conditions就可以出现。

想象一下,当线程A 读取了共享对象的count变量到它的CPU缓存中;另外一个线程B 也做了相同的事情,但是是到达不同的CPU缓存。现在,线程A 对count变量执行count 1 操作,线程B 也做了相同的事情。现在变量更新了2次,其中每个CPU缓存中一次。

如果更新操作顺序执行,那么count 变量将增加2次,将原始值 2写回到主内存。

然而,如果没有合适的同步化(synchronize),2次加1操作可能并发执行 。不管是线程A,还是线程B将其更新值写回主内存,count 变量也仅仅是在原始值的基础上执行count 1,而不是2次自增。

下图描绘了上述竞争条件(race conditions)的发生过程:

java内存模型包括什么(一篇文章彻底搞懂)(7)

为了解决这一问题,我们可以使用Java 同步块(synchronized)。同步块可以保证在任何给定时间,只有一个线程可以进入给定的代码临界段(或者同步块)。另外,Synchronized 块还保证在Synchronized块中访问的所有变量都将从主内存中读入,并且当线程退出Synchronized块是,所有的更新都将再次刷新回主内存中,无论变量是否修饰 volatile 或者没有。

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页