您的位置:首页 > 编程语言 > Java开发

Java Memory Model work with Hardware Memory Architecture

2017-01-13 09:55 302 查看
Java内存模型
Java内存模型
硬件存储结构
桥接Java内存模型和硬件存储结构
共享对象可见性
资源竞争

Java内存模型规定了Java虚拟机如何与机器内存(RAM)配合工作. Java虚拟机是整个电脑的一个模型,所以自然的也包含一个内存模型– 也称为Java内存模型.
理解Java内存模型对于设计正确的并发编程是非常重要的. Java内存模型指定如何及何时不同线程写入的共享对象值能被其他线程可见,并控制如何同步的获取共享对象。
基本的Java内存模型存在不足,所以Java内存模型在Java 1.5版本中做了修改. 这个版本的内存模型在Java 8中依然在使用.
Java内存模型
Java内存模型在JVM内部使用时将内存划分为线程栈和堆. 下图从逻辑视角阐明Java内存模型:



每个在Java虚拟机中运行的线程拥有自己的线程栈。线程栈包含到达当前运行点所调用的所有方法。我们将这称为“调用栈”,随着线程执行它的代码,调用栈将会变化。
线程栈也包含所有的每个被执行方法(所有在调用栈中的方法)的局部变量。一个线程只能访问它自己的线程栈。由一个线程创建的局部变量对所有其他线程都不可见,但本线程可见。甚至两个线程在执行相同的代码,它们也会在自己的线程栈中创建各自的局部变量。即每个线程含有自己版本的每个局部变量。
所有基本类型局部变量( boolean, byte, short, char, int, long, float, double) 完成存储在线程栈中,并且对于其他线程是不可见的。一个线程可以通过复制一个基本类型局部变量到其他线程,但不能共享基本类型局部变量本身。
堆中存放应用创建的所有对象,不管什么线程创建了这些对象。包含对象中基本包装类型(如. Byte, Integer, Long等等.)的版本号。不管一个对象被创建和分配给局部变量,或者做一个其他对象的成员被创建,这个对象都存储在堆中。
下图描述了存储在线程栈中的调用栈和局部变量,以及存储在堆中的对象:



一个局部变量可能是基本类型,这种情况它将完成被存储在线程栈中。
一个局部变量也可能是一个引用对象,这种情况引用(局部变量)被存储在线程栈,但是对象本身被存储在堆中。
一个对象可能包含方法,这些方法可能又包含局部变量。这些局部变量也存储在线程栈中,就算归属于方法的这些对象被存储在堆中。
一个对象的成员变量与该对象一起被存储在堆中,不管这个成员变量是基本类型或引用,都是这种存储情况。
静态类与类定义一起被存储在堆中
存在堆中对象能够被含有该对象引用的所有线程访问,当一个线程访问一个对象时,它也能访问该对象的成员变量。如果两个线程在同一时刻调用同一对象的一个方法,它们都能访问这个对象的成员变量,但是每个线程会拥有一份各自的局部变量拷贝。
下图描述了上面的这种情况:



两个线程拥有一组局部变量。其中一个局部变量(Local Variable 2)指向堆中的一个共享对象(Object 3)。这两个线程每个拥有一个不同的引用指向一个相同对象。它们的引用是局部变量存储在每个线程的线程栈中。这两个不同应用指向堆中的同一对象。
注意共享对象(Object 3)拥有成员变量Object 2和Object 4的一个引用(由图中Object 3到Object 2和Object 4箭头描述)。通过对象Object 3中的成员变量引用这两个线程可以访问Object 2 和 Object 4。
该图也展示了一个局部变量指向堆中的两个不同对象。在这个例子中引用指向了两个不同对象(Object 1和Object 5),不是同一个对象。理论上两个线程都可以访问Object 1和Object 5对象,如果两个线程都含有这两个对象引用。但在图中每个线程只含有这两个对象中的一个引用。
什么样类型的Java 代码能够产生上面这样的内存图形呢? 下面这个简单的代码可以展示:

public class MyRunnable implements Runnable() { 
    public void run() {
        methodOne();
    }
    public void methodOne() {
        int localVariable1 = 45; 
        MySharedObjectlocalVariable2 =
           MySharedObject.sharedInstance; 
        //... do more with localvariables. 
        methodTwo(); 
   } 
    public void methodTwo() {
        Integer localVariable1 = newInteger(99); 
        //... do more with localvariable.
    }

public class MySharedObject { 
    //static variable pointing toinstance of MySharedObject
     public static finalMySharedObject sharedInstance =
        new MySharedObject(); 
    //member variables pointing totwo objects on the heap
     public Integer object2 = newInteger(22);
    public Integer object4 = newInteger(44); 
    public long member1 = 12345;
    public long member1 = 67890;
}

如果这两个线程运行 run() 方法,那么将显示前面图表的结果. run()方法调用methodOne(), 然后 methodOne() 调用 methodTwo()。
methodOne() 声明了一个基本类型局部变量 (localVariable1的类型为int) 和一个引用类型的局部变量(localVariable2)。
每个线程执行methodOne() 方法将会再各自线程栈中产生localVariable1 和 localVariable2的一份拷贝。localVariable1 变量在两个线程中独立的存储在各自的线程栈中。一个线程将不能看见另外一个线程对localVariable1拷贝对象的更改。
每个线程执行 methodOne()方法也会产生localVariable2对象的拷贝. 然而,这两个不同的localVariable2 拷贝指向堆中的同一对象。设置localVariable2变量的代码将其指向了一个静态对象的引用。静态变量只有一份拷贝并且这份拷贝存储在堆中。这两个localVariable2 的拷贝指向了MySharedObject 的同一实例,也就是静态变量指向的对象。这个 MySharedObject 实例也是被存储在堆中。它对应于上图中的Object
3 对象。
注意到MySharedObject 类也包含了两个成员变量。这些成员变量跟对象本身一起被存储在堆中。这两个成员变量指向另外两个Integer 对象。这些Integer 对象对应于上图中的Object 2 和 Object 4。
注意到methodTwo() 也创建了一个命名为localVariable1的局部变量。这个局部变量是一个Integer 类型的引用对象。这个方法设置localVariable1 引用指向一个新的Integer 对象实例。每个执行methodTwo()方法的线程将会存储一份localVariable1对象引用的拷贝。这两个Integer 对象实例将会被存储在堆中,但是由于每次方法被执行时都会创建一个新的整数对象,两个线程执行此方法将创建独立的Integer实例。
这些在methodTwo()内部创建的Integer 对象对应于上图中的Object 1和Object 5。
注意到类MySharedObject 的两个成员变量是基本类型long。由于这些变量是成员变量,它们将跟对象一起被存储在堆中。只有局部变量被存储在线程栈中。
硬件存储结构
现代的硬件存储结构与java内存模型有所不同。了解硬件存储结构也是很重要的,从而了解java内存模型如何与它协调工作。本节描述了常见的硬件存储结构,后面的部分将描述java内存模型如何与它协调工作。
下图是一个简单的现代计算机硬件结构:



现代计算机通常有2个或更多的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内部寄存器中,下图描述了该情况:



当对象和变量被存储在电脑不同的内存区域,一些特定问题就会出现。主要有两个问题:
线程对共享对象更新(写入)的可见性。
对共享变量的读取、校验和写入的竞争冲突。
这些问题都在将下面章节进行介绍。
共享对象可见性
如果两个或更多线程在共享一个对象,没有合适的运用volatile声明和同步操作,一个线程对共享对象的更新其他线程将不可见。
想象共享对象被初始化存储在主存中。在CPU运行的A线程读取共享对象放入其CPU缓存。这时它对共享对象做了一个修改。只要CPU缓存没有被刷回主存,针对共享对象的版本修改将对运行在其他CPU上的线程不可见。这样每个线程拥有自己的共享对象副本,每个副本坐落在不同的CPU缓存中。
下图描述了上述勾画的情况。一个线程运行在左侧CPU,拷贝共享对象到其CPU缓存中,并且将count 变量值修改为2。这个修改对于运行在右侧CPU上的线程是不可见的,因为这个更新还没有被刷回主存。



解决这个问题可以使用Java关键字volatile。volatile关键字能保证一个给定变量直接从主存中读取,并且更新后马上写回主存。
资源竞争 
如果两个或多个线程共享一个对象,并且超过一个线程在更新共享变量中的变量,将会产生资源竞争。
想象如果线程A读取共享变量中的count 变量到它的CPU缓存中。线程B也做了同样操作,但将数据放在不同的CPU缓存。现在线程A将count 变量加1,并且线程B做了同样操作。现在var1 已经被增加了两次,在每个CPU缓存中各一次。
如果这些增加操作是顺序执行,则count 变量将被增加了两次,并且原始值被+2写回到主存中。
然而,由于没有适当的同步操作,这两次增加是被随机的执行。无论线程A或线程B中哪个线程将count 变量的更新版本号写回到主存,该更新值只会比原始值增加1,虽然有两次增量操作。
下图描述了上面所述的资源竞争冲突发生的情况:



解决这个问题需要用到Java的同步块操作,一个同步块保证在任何时刻只有一个线程能进入关键代码块。同步块保证在代码段中访问的所有变量将会直接从主存中读取,当线程退出同步块时所有的更新都会被写回主存,无论这些变量的定义是否被volatile修饰。


References

1、http://link.springer.com/chapter/10.1007%2F978-3-642-28869-2_25
2、http://www.journaldev.com/2856/java-jvm-memory-model-memory-management-in-java
3、http://jmm-wating-google-through-connect
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: