博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【09】Java 内存模型
阅读量:5938 次
发布时间:2019-06-19

本文共 5010 字,大约阅读时间需要 16 分钟。

  hot3.png

  • 原文地址:
  • 作者: Jakob Jenkov

Java 内存模型描述了JVM如何使用计算机内存(RAM)。 JVM是一个完整计算机的模型,在这个模型中自然就包括内存模型,这个内存模型就称之为JAVA内存模型

只有理解了Java内存模型,才能设计出一个恰当的并发程序。 因为Java内存模型中规范了线程是如何,以及什么时候才能看到其他线程写入的值。 模型中还规范了,在必要时,应该如何同步访问共享变量。

在Java 1.5 之前,Java内存模型存在诸多不足。所以,在Java 1.5中,对Java内存模型进行了重构。 1.5 改进后的Java内存模型也一直沿用到了 Java 8。

内部使用的Java内存模型

JVM内部将Java内存模型划分为线程栈两个模型。他们的逻辑关系,可以参见下图:

image

在JVM中运行的每一个线程都拥有自己的线程栈。 线程栈包含了被调用的方法中正在执行的相关信息。也可以称之为:调用栈。 随着线程的执行,调用栈也会随之改变。

线程栈还包含了每一个执行方法的局部变量。 线程只能访问自己的线程栈。一个线程创建的局部变量,对于其他线程是不可见的,即便是父线程也不能访问子线程的线程栈。 即使两个线程执行相同的代码,两个线程仍然是在各自的线程栈中创建局部变量。 因此,每一个线程都拥有各自版本的局部变量。

所有基本类型的局部变量(八个基本类型:booleanbyte,shortcharintlong,float,double)都是完全存放在线程栈中的。因此,对于其他线程都是不可见的。 一个线程可以将基本类型的值拷贝传递给另一个线程,但这不是将基本类型局部变量共享出去。(传递出去的只是一份拷贝)

堆中包含了Java应用中所有被创建的对象。不管是哪个线程创建的对象都在堆中。 这其中也包括基本类型的包装类(如:ByteIntegerLong等)。 不论是局部变量还是成员变量,只要是创建的对象,都会存放在堆中。

下图就展示了,栈中与堆中的存放的区别:

image

如果是基本类型的局部变量,那它完全存放于线程栈中。

如果局部变量是一个对象的引用,那引用本身是存放在线程栈中的,而对象是存放在堆中的。(Java中都是值传递

对象中会有方法,而这些方法会有一些局部变量。对于这些局部变量,仍然是存放于栈中的,而不是跟着对象一起存放于堆中。

对象中的成员变量,会随着对象一起存放在堆中。不论成员变量是基本类型还是对象引用都是在堆中。

静态变量则是随着class定义一起存放在堆中(与类信息在一起,而不是对象)。

在堆中的对象可以被所有持有引用的线程访问到。 当线程获得一个对象时,线程其实也就可以访问对象的成员变量了。 而如果两个线程同时调用同一个对象的同一个方法时,就意味着,两个线程可以同时访问对象的成员变量,而局部变量则是两个线程访问各自的拷贝。如图:

image

两个线程都有着各自的局部变量。但是Local Variable 2指向的是一个在堆中的共享对象(Object 3)。 两个线程拿着指向同一个对象的两个引用。虽然引用是局部变量,是存放在各自的线程栈中。但是,这两个引用却指向的是堆中同一个对象。这样就可能导致并发问题了。

注意,共享对象(Object 3)还有两个成员变量(Object 2Object 4)。通过成员变量引用,两个线程其实还可以同时访问Object 2Object 4

图中,还展示了局部变量指向堆中两个不同的对象(Object 1Object 5)。虽然堆中的对象是共享的,但是两个线程各自只拿着一个对象的引用,所以(Object 1Object 5)不能被另一个线程访问到。

下面的代码就展示了图中的逻辑:

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()方法,那上图中的情况就会发生。 run()方法调用methodOne()methodOne()则会调用methodTwo()

methodOne()定义了一个基本类型的局部变量(int:localVariable1)以及一个引用型的局部变量(ref:localVariable2)。

每一个执行methodOne()的线程都会创建一个localVariable2的拷贝。但是两份拷贝都是指向堆中的同一个对象。 都是指向静态变量MySharedObject.sharedInstance。 两个线程都是对堆中的一个引用进行拷贝。 因此,localVariable2的两份拷贝,都是指向静态变量对应的同一个MySharedObject实例。 而MySharedObject实例也是在堆中的,所以,这个实例就类似上图中的Object 3。

MySharedObject类中,又包含两个成员变量。这些成员变量会随着对象一起存放于堆中。 这两个成员变量指向两个Integer对象,这就类似上图中的Object 2 和 Object 4。

methodTwo()方法,又会创建一个叫做localVariable1的局部变量。这个局部变量是一个Integer对象引用。 每一个调用methodTwo()的线程都会持有一个属于自己的localVariable1引用拷贝。 虽然,两个localVariable1对象都是存放在堆中,但是每个线程都是各自创建的对象实例。这样localVariable1就类似上图中的Object 1 和 Object 5。

MySharedObject类中,还有两个long类型的成员变量。虽然是基本类型,但是是成员变量,所以还是随着对象一起存放于堆中。 只有局部变量才会存放在线程栈中。

硬件中的内存模型

现代硬件中的内存模型会和Java内存模型有些不一样。 所以,除了需要了解Java内存模型,还需要了解硬件中的内存模型。这样才能更好的理解Java内存模型是如何工作的。 先来看看硬件内存模型的结构:

image

现在的计算机通常有着两个以上的CPU。而这些CPU里面还会有着多个内核。 关键的是,现代计算机上的多个CPU还支持同时运行多个线程。 这就意味着,如果Java应用是多线程的,就可以更好的利用每一个CPU上的多线程运行能力,从而获得更好的性能。

每一个CPU都包含一组寄存器(本质上也是CPU内部的内存)。 CPU对于寄存器的访问要比访问内存快很多。

每一个CPU还会有一个缓存层。事实上,大多数现代CPU会有着多层缓存。 CPU访问这些缓存要比内存快,但是比访问寄存器要慢一些。 因此,CPU缓存的访问速度,介于寄存器与内存之间。 有些CPU会有多层缓存(Level 1 以及 Level 2,甚至 Level 3),但是这些对于理解Java内存模型是如何工作的并不重要。 重要的是理解CPU内部有着多层缓存结构。

计算机中的主存区(RAM),也就是通俗意义上的内存。这些主存,是可以被所有的CPU访问的。 主存通常比CPU缓存要大很多。

比如说,当CPU需要访问主存,它会读取主存的某个小部分数据到CPU缓存中。 甚至可能,直接读取缓存中的数据到寄存器中,然后对这些数据进行操作。 当CPU需要将数据写回到主存时,它会将数据从寄存器中刷新到缓存,然后在某个时间点,写回主存。

当CPU需要存放新的数据到缓存中时,缓存中之前存放的数据就会写回到主存。 CPU缓存可能一次只从主存中读取一部分数据,也可能一次只写回一部分数据到主存中。 CPU并不会每次读写操作时,都对全部的缓存进行同步操作。 而是将缓存分块与内存块进行对应,从而完成缓存到内存的更新。这些缓存块,被称之为“缓存行(cache lines)”。 缓存行可以被读入到缓存中,然后这些缓存行在操作完成后,被写回主存。

Java内存模型与硬件内存模型的对应关系

正如上文中介绍的,Java内存模型与硬件内存模型是不一样的。所以,硬件内存模型不能区分线程栈和堆的。 对于硬件来说,线程栈和堆都是在主存中。在某些时刻,部分线程栈或者部分堆中的数据会进入到CPU缓存或者CPU寄存器中。

image

对象和变量可以被存放在不同的内存区域,这样就会有一些问题。其中最主要的两个是:

  • 线程对共享变量进行写操作后的数据可见性
  • 对共享变量进行读,检查,写等操作时的竞态条件

共享对象的可见性

如果多个线程持有同一个共享对象,而且还没使用volatile关键字,也没有使用同步块。那这种情况下,对共享对象的修改就可能对其他线程是不可见的。

想象一下,共享对象最初是存放在主存中的。一个线程在其中一个CPU上进行了读取,这个对象数据就会被读取到这个CPU的缓存中。然后这个线程对其进行了修改。这样,只要CPU缓存没有写回主存中,那这个修改对于其他线程,是看不见的。 这样就会出现,每个线程都拿着各自的副本,而这些副本还可能分布在不同的CPU缓存中。

下面这个图,展示了这类的问题:

image

一个线程在左边的CPU上运行,其中有一个共享对象的副本读取到了CPU缓存中,并且,将count修改成了2。 这个修改对于右边CPU上运行的线程是不可见的,因为,count的修改还没有写回主存。

要解决这个问题,可以使用volatile关键字。volatile可以保障变量直接从主存中读取,并且保障修改后可以直接写回主存。

竞态条件

如果多个线程修改了同一个共享变量,那就回触发竞态条件。

想象一下,如果线程A读取了变量count到CPU缓存中。 而线程B也读取了,但是是在另一个CPU的缓存中。 那线程A在count上加一,线程B也对它的count加一。 那么变量已经在不同的CPU缓存上执行了两次递增操作。

如果上述操作是串行执行的,那count会在原有数值上递增两次,相当与加二,然后写回主存。

但是,两次递增操作在没有同步机制的约束下,就可能是并发的。 不管线程A和B谁来更新count并写回主存,新值都可能是只加了一,而不是加二。

如下图所示:

image

要解决这个问题,就需要使用Java的synchronized块。同步块可以保障一次只有一个线程能够进入临界区。 并且还能保障同步块中所有从主存中读取的变量,在线程退出同步块的时候,都可以安全的写回主存。

转载于:https://my.oschina.net/roccn/blog/1503397

你可能感兴趣的文章
java_分数
查看>>
守护线程与非守护线程
查看>>
Js中parentNode,parentElement,childNodes,children之间的区别
查看>>
JS复习:第三章&第四章
查看>>
webpack的问题;
查看>>
如何用JS获取ASP.net中的textbox的值 js获不到text值,【asp.net getElementById用法】
查看>>
ASP.NET弹出对话框几种基本方法
查看>>
正阳门下
查看>>
【01】Python:故事从这里开始
查看>>
理解Underscore中的_.bind函数
查看>>
关于目标检测 Object detection
查看>>
vue-cli 3.0 axios 跨域请求代理配置及生产环境 baseUrl 配置
查看>>
Morris Traversal
查看>>
随机数的扩展--等概率随机函数的实现
查看>>
UVA-10347 Medians 计算几何 中线定理
查看>>
eclipse中怎么删除重复的console
查看>>
软件工程(2019)结对编程第二次作业
查看>>
平安人寿保险-深圳Java开发工程师社招面试
查看>>
编辑距离问题
查看>>
Python_练习题_49
查看>>