java内存模型

来源:互联网 发布:猪肉绦虫知乎 编辑:程序博客网 时间:2024/06/05 19:10

java内存模型指明了java虚拟机如何利用计算机内存(RAM)工作的。因为java虚拟机是整个计算机的一个模型,那么很自然的他也包含一个内存模型。
如果你想设计一个正确稳定的并发程序,那么理解java内存模型将会对此有很大的帮助。因为java内存模型指明了不同线程怎样和在什么时候能看到其他线程的共享变量被写入了,以及怎样能够成功地同步访问共享变量。
最原始的java内存模型是不足的,所以java内存模型在1.5的时候被改进了,并且该版本一直沿用到java8
java内存模型的内部构造:
java内存模型在JVM里面将内存分为线程栈(thread stack)和堆(heap)
以下从逻辑的角度用图表解释了java内存模型
java内存模型的内部构造
每一个线程在jvm里面都有自己的线程栈。线程栈包含了有关线程调用的方法信息去达到当前的执行点,我们更愿意叫他调用栈。因为线程执行代码,调用栈也一直在变化。
线程栈里面也包含了当前每一个方法的所有本地变量。一个线程只能访问自己的线程栈,线程所创建的局部变量对于所有其他线程都是不可见的。即使两个线程正在执行同一个代码,他们依然会在自己的线程栈里面创建自己的本地变量。因此,每一个线程都有自己的本地变量版本
原始类型(boolean,byte,short,char,int,long,float,double)的所有局部变量都完全存储在线程堆栈上,因此对其他线程不可见。一个线程可以将一个pritimive变量的副本传递给另一个线程,但是它不能共享原始局部变量本身
堆包含您的Java应用程序中创建的所有对象,而不管创建对象的线程是什么。这包括基本类型的对象版本(例如,字节,整数,长等)。不管对象被创建并分配给局部变量,或者创建为另一个对象的成员变量,对象仍然存储在堆上。
这是一个图示出了存储在线程堆栈上的调用堆栈和局部变量以及存储在堆上的对象:
这里写图片描述
局部变量可能是一个原始类型,在这种情况下,它完全保留在线程栈上。局部变量也可以是对象的引用,在这种情况下,引用(局部变量)存储在线程堆栈上,但对象本身存储在堆上。对象可能包含方法,这些方法可能包含局部变量。这些局部变量也存储在线程堆栈中,即使该方法所属的对象存储在堆上。
当成员变量是原始类型,并且它是对对象的引用时,对象的成员变量与对象本身一起存储在堆上。
静态类变量也与类定义一起存储在堆上。
所有对该对象引用的线程都可以访问堆上的该对象。当线程访问对象时,它也可以访问对象的成员变量。如果两个线程同时在同一个对象上调用一个方法,则它们都可以访问对象的成员变量,但每个线程都有自己的局部变量副本。
以下是上图所示的图:
这里写图片描述
两个线程有​​一组局部变量。局部变量(Local Variable 2)之一指向堆上的共享对象(对象3)。两个线程各自对同一个对象有不同的引用。它们的引用是局部变量,因此存储在每个线程的线程堆栈(每个)上。但是两个不同的引用指向堆上的同一个对象。
注意共享对象(Object 3)如何引用Object 2和Object 4作为成员变量(由Object 3到Object 2和Object 4的箭头所示)。通过对象3中的这些成员变量引用,两个线程可以访问对象2和对象4。
该图还显示了一个局部变量,指向堆上的两个不同对象。在这种情况下,引用指向两个不同的对象(对象1和对象5),而不是相同的对象。在理论上,如果两个线程都对两个对象都引用,则两个线程都可以访问对象1和对象5。但是在上图中,每个线程只有一个对两个对象之一的引用。
那么,什么样的Java代码可以导致上述内存图?那么代码如下代码一样简单:

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)和一个作为对象引用(localVariable2)的局部变量。执行methodOne()的每个线程将在它们各自的线程堆栈上创建自己的localVariable1和localVariable2副本。 localVariable1变量将完全分开,只能生活在每个线程的线程堆栈上。一个线程无法看到另一个线程对其localVariable1的副本进行了什么更改。执行methodOne()的每个线程也将创建自己的localVariable2副本。然而,localVariable2的两个不同的副本都最终指向堆上的同一个对象。代码将localVariable2设置为指向静态变量引用的对象。静态变量只有一个副本,该副本存储在堆上。因此,localVariable2的两个副本都指向静态变量指向的MySharedObject的同一个实例。 MySharedObject实例也存储在堆上。它对应于上图中的对象3。注意MySharedObject类如何包含两个成员变量。成员变量本身与对象一起存储在堆上。两个成员变量指向另外两个Integer对象。这些整数对象对应于上图中的对象2和对象4。
还要注意methodTwo()如何创建一个名为localVariable1的局部变量。这个局部变量是一个Integer对象的对象引用。该方法将localVariable1引用设置为指向一个新的整数实例。 localVariable1引用将被存储在执行methodTwo()的每个线程的一个副本中。实例化的两个Integer对象将被存储在堆上,但是由于该方法在每次执行该方法时都会创建一个新的Integer对象,所以执行此方法的两个线程将创建单独的Integer实例。 methodTwo()中创建的Integer对象对应于上图中的Object 1和Object 5。
还要注意类型为long的类MySharedObject中的两个成员变量,这是一个原始类型。由于这些变量是成员变量,它们仍然与对象一起存储在堆上。只有局部变量存储在线程堆栈中。
硬件内存架构
现代硬件内存架构与内部Java内存模型有所不同。了解硬件内存架构也很重要,以了解Java内存模型的工作原理。本节介绍常见的硬件内存架构,后面的部分将介绍Java内存模型的工作原理。
以下是现代计算机硬件架构的简化图:
这里写图片描述
现代计算机通常有2个或更多的CPU。其中一些CPU也可能有多个内核。关键是,在具有2个或更多个CPU的现代计算机上,可以同时运行多个线程。每个CPU都可以在任何给定的时间运行一个线程。这意味着如果您的Java应用程序是多线程的,则每个CPU可能会在Java应用程序中同时(并行)运行一个线程。
每个CPU都包含一组本质上是CPU内存的寄存器。 CPU可以在这些寄存器上执行的操作比对主存储器中变量执行的操作要快得多。这是因为CPU可以访问这些寄存器比访问主内存的速度快得多。
每个CPU也可以具有CPU缓存存储器层。事实上,大多数现代CPU具有一定大小的缓存内存层。 CPU可以比主存储器快速访问其缓存,但通常不能像访问其内部寄存器一样快。因此,CPU缓存内存位于内部寄存器和主存储器的速度之间。某些CPU可能有多个缓存层(1级和2级),但是要了解Java内存模型如何与内存进行交互,这并不重要。重要的是知道CPU可以具有某种缓存内存层。
计算机还包含主存储区(RAM)。所有CPU都可以访问主存储器。主存储区通常远大于CPU的高速缓冲存储器。
通常,当CPU需要访问主存储器时,它会将主存储器的一部分读入其CPU缓存。甚至可以将部分高速缓存读入其内部寄存器,然后对其执行操作。当CPU需要将结果写回主内存时,它将从内部寄存器中将值刷新到高速缓冲存储器,并在某些时候将值刷新到主存储器。
当CPU需要在高速缓冲存储器中存储其他内容时,存储在高速缓冲存储器中的值通常被刷新回主存储器。 CPU缓存每次可以将数据写入其内存的一部分,并一次刷新其内存的一部分。每次更新时,它不必读取/写入完整的缓存。通常,缓存在被称为“高速缓存行”的更小的存储块中被更新。可以将一个或多个高速缓存行读入高速缓冲存储器,并且可以将一个或多个高速缓存线重新刷回主存储器。
弥合Java内存模型与硬件内存架构之间的差距
如前所述,Java内存模型和硬件内存架构是不同的。硬件内存架构不区分线程堆栈和堆。在硬件上,线程堆栈和堆都位于主内存中。线程堆栈和堆的一部分有时可能存在于CPU高速缓存和内部CPU寄存器中。这在图中说明:
这里写图片描述
当对象和变量可以存储在计算机的各种不同的存储区域中时,可能会出现某些问题。两个主要问题是:
1.线程更新(写入)到共享变量的可见性。
2.读取,检查和写入共享变量时的竞争条件。
这两个问题将在以下部分中解释。
共享对象的可见性
如果两个或多个线程共享一个对象,没有正确使用volatile声明或同步,一个线程所做的共享对象的更新可能对其他线程是不可见的。
假设共享对象最初存储在主内存中。在CPU 1上运行的线程将共享对象读入其CPU缓存。在那里它对共享对象进行了更改。只要CPU缓存没有被刷新到主内存,共享对象的更改版本对于在其他CPU上运行的线程是不可见的。这样一来,每个线程可能会以自己的共享对象副本结束,每个副本都坐在不同的CPU缓存中。
下图说明了草图的情况。在左CPU上运行的一个线程将共享对象复制到其CPU缓存中,并将其计数变量更改为2.对于正确CPU上运行的其他线程,此更改不可见,因为更新计数尚未刷新到主记忆还没
这里写图片描述
要解决这个问题,您可以使用Java的volatile关键字。 volatile关键字可以确保给定的变量直接从主内存中读取,并在更新时总是写回主内存。
竞争条件
如果两个或多个线程共享对象,并且多个线程更新该共享对象中的变量,则可能会发生竞争条件。
假设线程A将共享对象的变量计数读入其CPU缓存中。想像一下,那个线程B也是一样的,而是进入不同的CPU缓存。现在线程A增加一个计数,线程B也是一样的。现在,var1已经增加了两次,每次CPU缓存一次。如果这些增量依次执行,则变量计数将被增加两次,并将原始值+ 2写回到主存储器。但是,两个增量在没有正确同步的情况下同时进行。不管线程A和B哪个将其更新版本的计数写回到主内存,尽管有两个增量,更新的值将仅比原始值高1。
该图说明了如上所述的竞争条件的问题的发生:
这里写图片描述
要解决此问题,您可以使用Java同步块。同步块保证在任何给定时间只有一个线程可以进入代码的给定关键部分。同步块还保证在同步块内访问的所有变量将从主存储器读入,并且当线程退出同步块时,所有更新的变量将被刷新回主存储器,而不管该变量是否被声明为volatile或不。

原创粉丝点击