摘要:内存模型指定了如何与计算机内存协同工作。内部的内存模型内存模型在内部使用,将内存分为了线程栈和堆。下面的图从逻辑角度给出了内存模型每个运行在内部的线程都有自己的线程栈。部分线程栈和堆可能在某些时候会占用缓存和内部寄存器。
Java内存模型指定了JVM如何与计算机内存协同工作。JVM是整个计算机的模型因此这个模型包含了内存模型,也就是Java内存模型。
如果你像要设计正确行为的并发程序,那么了解Java内存模型是非常重要的。Java内存模型指定了如何以及何时不同的线程能够看到其他线程写入共享变量的值,以及如何在需要的时候如何同步访问共享变量。
最初的Java内存模型是不足的,因此Java内存模型在Java1.5做了改进,这个版本的Java内存模型在Java8中仍然被使用。
内部的Java内存模型Java内存模型在JVM内部使用,将内存分为了线程栈和堆。下面的图从逻辑角度给出了Java内存模型:
每个运行在JVM内部的线程都有自己的线程栈。线程栈包含关于线程调用的哪个方法到达了当前执行点的信息。我对此引用为“调用栈”。随着线程执行代码,调用栈会发生变化。
调用栈还包含每个被执行的方法的所有本地变量(所有调用栈上的方法)。一个线程只能够访问它自己的线程栈。由一个线程创建的本地变量对其他线程不可见。即使两个线程执行同一段代码,这两个线程也会在他们各自的线程栈中创建这段代码涉及的本地变量。因此,每个线程都有自己版本的本地变量。
所有内建类型的本地变量(boolean,byte,short,char,int,long,float,double)被存储在线程栈并且对其他线程不可见。一个线程可能会传递一个内建类型变量的副本给其他线程,但是它不会贡献它自己的内建本地变量。
堆包含了你的Java程序中创建的所有对象,不管是哪个线程创建的。这包含了对象版本的内建类型(如Byte,Integer,Long等等)。如果一个对象呗创建并被复制给一个本地变量,或者被创建为一个成员变量都是没关系的,对象仍然存储在堆上。
下图给出了调用栈和存储在线程栈中的本地变量,以及存储在堆上的对象:
一个本地变量可能是一个内建类型,这种情况它完全存储在线程栈。
一个本地变量可能是一个对象的引用。这种情况这个引用(本地变量)存储在线程栈中,但是对象本身存储在堆上。
一个对象可能包含方法,并且这些方法可能包含本地变量。这些本地变量存储在线程栈,即使方法所属对象存储在堆上。
一个对象的成员变量和对象一起存储在堆上。对于成员变量是内建类型,或者它是对象的引用都是如此。
静态类变量和类定义一起存储在堆上。
堆上的对象能够被所有拥有这个对象引用的线程访问。当一个线程访问一个对象,它也可以访问这个对象的成员变量。如果两个线程在同一个对象上同时调用它的同一个方法,这两个线程会同时又权限访问这个对象的成员变量,但是每个线程会有它自己的本地变量副本。
下面的图给出了上面所说的:
两个线程有同一组本地变量。一个本地变量(Local Variable 2)指向了堆上的一个共享对象(Object3)。每个线程都有对同一个对象的不同引用。它们的引用是本地变量并且存储在各自的线程栈上,尽管这两个不同的引用指向堆上的同一个对象。
注意共享对象(Object 3)有一个对Object2和Object4的引用作为它的成员变量,通过Object3中的这些成员变量引用,这两个线程可以访问Object2和Object4。
图中还给出了一个本地变量指向堆上的两个不同的对象。这个例子中引用指向了两个不同对象(Object1和Object5),而不是同一个对象。理论上所有线程如果有指向所有有对象的引用,那么这些线程可以访问到Object1和Object5。但是在图中每个线程只有一个引用指向这两个对象之一。
那么,什么样的Java代码能够满足上面的内存图示?请看下面的简单代码:
public class MyRunnable implements Runnable { public void run() { methodOne(); } public void methodOne() { int localVariable1 = 45; MyShareObject localVariable2 = MyShareObject.shareInstance; // ... do more with local variables. methodTwo(); } public void methodTwo() { Integer localVariable1 = new Integer(99); // ... do more with local variable. } }
public class MyShareObject { // static variable pointing to instance of MyShareObject public static final MySharedObject sharedInstance = new MySharedObject(); // member variable 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 member2 = 67890; }
如果两个线程执行run()方法,则图中所示就是结果。run()方法调用methodOne()然后methodOne()调用methodTwo()。
methodOne()声明了一个内建类型的本地变量(int类型的localVariable1),另一个本地变量是一个对象的引用(localVariable2)。
每个执行methodOne()的线程会在各自的线程栈上创建它自己的localVariable1和localVariable2的副本。两个localVariable1变量完全和对方没有关系,只是活在各自的线程栈上。一个线程不能看到另一个线程它自己的localVariable1副本变化。
每个执行methodOne()的线程也会在各自的线程栈上创建它们自己的localVariable2副本。然而这两个不同的localVariable2副本是指向堆上的同一个对象。代码设置localVariable2指向被一个静态变量引用的对象。这里只有一个静态变量的副本并且这个副本存储在堆上。因此所有localVariable2的这两个副本都指向同一个被静态变量指向的MySharedObject实例。MySharedObject实例存储在堆上,它对应图上的Object3。
注意MySharedObject类还包含了两个成员变量。成员变量和这个对象一起存储在堆上。这两个成员变量指向了两个Integer对象。这些Integer对象对应图上Object2和Object4。
注意methodTwo()创建了一个名为localVariable1的本地变量,这个本地变量是一个Integer对象的引用。这个方法设置localVariable1引用指向了一个新的Integer实例。localVariable1引用会存储在执行methodTwo()方法的每个线程的副本中。两个被实例化的Integer对象会存储在堆中,但是由于每次方法执行时都创建了一个新的Integer对象,两个线程会执行并创建两个不同的Integer实例。methodTwo()中创建的Integer对象对应图中的Object1和Object5。
注意MySharedObject中的两个long型的成员变量是内建类型。由于这些变量的成员变量,因此它们仍然和对象一起存储在堆上。只有本地变量会存储在线程栈上。
硬件内存架构现代硬件内存架构和内部Java内存模型有些区别。对于了解Java内存模型如何工作,了解硬件内存架构也很重要。这部分描述通用硬件内存架构,下一个部分会描述Java内存模型是如何工作在硬件内存之上。
这里有一个简单的计算机硬件架构模型:
现代计算机通常有2个或更多的CPU。有些CPU还有多个核。重点是,在一个有2个或更多CPU的计算机上,有多个线程同时运行是可能的。每个CPU能够在任何时候运行一个线程。这意味着如果你的Java程序是多线程的,每个CPU一个线程同时并发运行在你的Java程序中。
每个CPU包含一组寄存器,本质行是CPU内的存储。CPU在这些寄存器中执行操作会比在主存中快的多。这是因为CPU能够更快的访问这些寄存器。
每个CPU可能还有一个CPU缓存层。实际上,大部分现代CPU都有一个特定大小的缓存层。CPU能比访问主存更快的访问缓存,但是一般不会比访问它的内部寄存器更快。因此,CPU缓存是一个介于内部寄存器和主存之间的地方。有些CPU可能有多级缓存(Level1和Level2),但是这对理解Java内存模型如何与内存交互来说并不是很需要知道。
一个计算机也包含一个主存区域(RAM)。所有CPU都能访问主存。主存区域比CPU缓存大的多。
一般来说,当一个CPU需要访问主存,它会将主存的一本读取到它的CPU缓存。甚至它可能会读取部分缓存到它的内部寄存器并在其上操作。当CPU需要将结果写回到主存它会将值从内部寄存器刷到缓存,在摸个时间点将缓存中的值刷回到主存。
当CPU需要在缓存中存储一些其他东西时,缓存中存储的值会被刷回到主存。每次缓存更新时,CPU不必读写整块缓存。对于缓存在较小内存块上的更新的标准说法是“cache lines”。一个或多个cache lines会被读到缓存,一个或多个cache lines会被刷回主存。
连接Java内存模型和硬件内存架构上面说道,Java内存模型和硬件内存架构不同。硬件内存架构不会分辨线程栈和堆。在硬件上,线程栈和堆都定位到主存。部分线程栈和堆可能在某些时候会占用CPU缓存和内部CPU寄存器。如下图所示:
当对象和变量能被存储在计算机的不同内存区域时,特定的问题就会发生。两个主要问题是:
线程更新(写)到共享变量的可见性
读写检查共享变量时发生的竞态条件
这些问题会在下面的部分解释。
共享变量的可见性如果两个或多个线程共享一个对象,如果没有恰当使用volatile声明或者同步,一个线程对共享变量的更新对其他线程可能会不可见。
想象一个共享对象初始存储在主存。一个运行在CPU1上的线程将这个共享变量读取到它的CPU缓存,然后对这个共享变量做一些改变,只要CPU缓存没有被刷回主存,这个共享变量的变更版本对运行在其他CPU上的线程就是不可见的。这种方式每个线程会有这个共享变量的本地副本,每个副本位于不同的CPU缓存中。
下图展示了这种情况。运行在左边CPU的线程将共享变量拷贝到它的CPU缓存,并将这个对象的count变量变为2.这个变化对运行在右边CPU上的线程不可见,因为对count的更新还没有刷回主存。
为了解决这个问题,你可以使用Kava的volatile关键字。volatile关键字能够保证一个给定的变量从主存中读取,并且当变量更新时会写回主存。
竞态条件如果两个或多个线程共享一个对象,多余一个线程更新这个共享对象的变量,静态条件就可能发生。
想象如果线程A读取了一个共享对象的count变量到它的CPU缓存,线程B做同样的事情,但是是在一个不同的CPU缓存。现在线程A对count加1,线程B也对count加1.现在count被加了两次,每次都是在不同的CPU缓存。
如果这些增加的操作被顺序执行,那么变量count会增加两次并有初始值+2的值被写回主存。
但是这两次增加是在没有同步的情况下并发操作的。不管线程A还是线程B将它们对count的更新版本写回主存,count只会得到初始值+1,尽管有两次更新。
下面的图描述了静态条件:
为了解决这个问题你可以用一个synchronized块。一个synchronized块保证了同时只有一个线程能进入一个给定的关键代码区域。synchronized块也保证了所有在synchronized块中访问的变量会从主存中读取,当一个线程退出synchronized块,所有对变量的更新会再次刷回主存,不管这个变量是否被声明为volatile。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/75958.html
摘要:因为管理人员是了解手下的人员以及自己负责的事情的。处理器优化和指令重排上面提到在在和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。有没有发现,缓存一致性问题其实就是可见性问题。 网上有很多关于Java内存模型的文章,在《深入理解Java虚拟机》和《Java并发编程的艺术》等书中也都有关于这个知识点的介绍。但是,很多人读完之后还是搞不清楚,甚至有的人说自己更懵了。本文,就来整体的...
摘要:因为管理人员是了解手下的人员以及自己负责的事情的。处理器优化和指令重排上面提到在在和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。有没有发现,缓存一致性问题其实就是可见性问题。 网上有很多关于Java内存模型的文章,在《深入理解Java虚拟机》和《Java并发编程的艺术》等书中也都有关于这个知识点的介绍。但是,很多人读完之后还是搞不清楚,甚至有的人说自己更懵了。本文,就来整体的...
摘要:编译器,和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。正确同步的多线程程序的执行将具有顺序一致性程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。 前情提要 深入理解Java内存模型(六)——final 处理器内存模型 顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照。JMM和处理器内...
摘要:内存模型即,简称,其规范了虚拟机与计算机内存时如何协同工作的,规定了一个线程如何和何时看到其他线程修改过的值,以及在必须时,如何同步访问共享变量。内存模型要求调用栈和本地变量存放在线程栈上,对象存放在堆上。 Java内存模型即Java Memory Model,简称JMM,其规范了Java虚拟机与计算机内存时如何协同工作的,规定了一个线程如何和何时看到其他线程修改过的值,以及在必须时,...
摘要:作为一个程序员,不了解内存模型就不能写出能够充分利用内存的代码。程序计数器是在电脑处理器中的一个寄存器,用来指示电脑下一步要运行的指令序列。在虚拟机中,本地方法栈和虚拟机栈是共用同一块内存的,不做具体区分。 作为一个 Java 程序员,不了解 Java 内存模型就不能写出能够充分利用内存的代码。本文通过对 Java 内存模型的介绍,让读者能够了解 Java 的内存的分配情况,适合 Ja...
阅读 3009·2021-11-19 11:31
阅读 3088·2021-09-02 15:15
阅读 952·2019-08-29 17:22
阅读 1033·2019-08-29 16:38
阅读 2446·2019-08-26 13:56
阅读 811·2019-08-26 12:16
阅读 1388·2019-08-26 11:29
阅读 910·2019-08-26 10:12