Java并发编程(四)-- Java内存模型

Java内存模型

前面讲到了Java线程之间的通讯采用的是共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每一个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化。java

Java内存模型即Java Memory Model,简称JMM。JMM规范了Java 虚拟机(JVM)在计算机内存(RAM)是如何协同工做的。Java虚拟机是一个完整的计算机虚拟模型,所以这个模型天然也包含一个内存模型——称为Java内存模型。也就是说JMM是隶属于JVM的。原始的Java内存模型存在一些不足,所以Java内存模型在Java1.5时被从新修订,如今的Java8仍沿用了Java1.5的版本。程序员

若是你想设计表现良好的并发程序,理解Java内存模型是很是重要的。Java内存模型规定了如何和什么时候能够看到由其余线程修改事后的共享变量的值,以及在必须时如何同步的访问共享变量。例如缓存

从上图来看,线程A与线程B之间如要通讯的话,必需要经历下面2个步骤:多线程

  •  首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  • 而后,线程B到主内存中去读取线程A以前已更新过的共享变量。

下面经过示意图来讲明这两个步骤: 架构

本地内存A和B有主内存中共享变量x的副本,假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在本身的本地内存A中。当线程A和线程B须要通讯时,线程A首先会把本身本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。并发

从总体来看,这两个步骤实质上是线程A在向线程B发送消息,并且这个通讯过程必需要通过主内存,由于JMM经过控制主内存与每一个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。优化

Java内存模型内部原理

Java内存模型只是一个抽象概念,那么它在Java中具体是怎么工做的呢?为了更好的理解上Java内存模型工做方式,下面就JVM对Java内存模型的实现、硬件内存模型及它们之间的桥接作详细介绍。spa

Java内存模型把Java虚拟机内部划分为线程栈和堆。下图演示了Java内存模型的逻辑视图。线程

每个运行在Java虚拟机里的线程都拥有本身的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问本身的线程栈。一个线程建立的本地变量对其它线程不可见,仅本身可见。即便两个线程执行一样的代码,这两个线程任然在在本身的线程栈中的代码来建立本地变量。所以,每一个线程拥有每一个本地变量的独有版本。设计

全部原始类型的本地变量都存放在线程栈上,所以对其它线程不可见。一个线程可能向另外一个线程传递一个原始类型变量的拷贝,可是它不能共享这个原始类型变量自身。

堆上包含在Java程序中建立的全部对象,不管是哪个对象建立的。这包括原始类型的对象版本。若是一个对象被建立而后赋值给一个局部变量,或者用来做为另外一个对象的成员变量,这个对象任然是存放在堆上。

下面这张图演示了调用栈和本地变量存放在线程栈上,对象存放在堆上。

一个本地变量多是原始类型,在这种状况下,它老是“呆在”线程栈上。

一个本地变量也多是指向一个对象的一个引用。在这种状况下,引用(这个本地变量)存放在线程栈上,可是对象自己存放在堆上。

一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即便这些方法所属的对象存放在堆上。

一个对象的成员变量可能随着这个对象自身存放在堆上。无论这个成员变量是原始类型仍是引用类型。

静态成员变量跟随着类定义一块儿也存放在堆上。

存放在堆上的对象能够被全部持有对这个对象引用的线程访问。当一个线程能够访问一个对象时,它也能够访问这个对象的成员变量。若是两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,可是每个线程都拥有这个本地变量的私有拷贝。以下图:

两个线程拥有一些列的本地变量。其中一个本地变量(Local Variable 2)执行堆上的一个共享对象(Object 3)。这两个线程分别拥有同一个对象的不一样引用。这些引用都是本地变量,所以存放在各自线程的线程栈上。这两个不一样的引用指向堆上同一个对象。

注意,这个共享对象(Object 3)持有Object2和Object4一个引用做为其成员变量(如图中Object3指向Object2和Object4的箭头)。经过在Object3中这些成员变量引用,这两个线程就能够访问Object2和Object4。

这张图也展现了指向堆上两个不一样对象的一个本地变量。在这种状况下,指向两个不一样对象的引用不是同一个对象。理论上,两个线程均可以访问Object1和Object5,若是两个线程都拥有两个对象的引用。可是在上图中,每个线程仅有一个引用指向两个对象其中之一。

什么类型的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()声明了一个原始类型的本地变量和一个引用类型的本地变量。

每一个线程执行methodOne()都会在它们对应的线程栈上建立localVariable1和localVariable2的私有拷贝。localVariable1变量彼此彻底独立,仅“生活”在每一个线程的线程栈上。一个线程看不到另外一个线程对它的localVariable1私有拷贝作出的修改。

每一个线程执行methodOne()时也将会建立它们各自的localVariable2拷贝。然而,两个localVariable2的不一样拷贝都指向堆上的同一个对象。代码中经过一个静态变量设置localVariable2指向一个对象引用。仅存在一个静态变量的一份拷贝,这份拷贝存放在堆上。所以,localVariable2的两份拷贝都指向由MySharedObject指向的静态变量的同一个实例。MySharedObject实例也存放在堆上。它对应于上图中的Object3。

MySharedObject类也包含两个成员变量。这些成员变量随着这个对象存放在堆上。这两个成员变量指向另外两个Integer对象。这些Integer对象对应于上图中的Object2和Object4.

methodTwo()建立一个名为localVariable的本地变量。这个成员变量是一个指向一个Integer对象的对象引用。这个方法设置localVariable1引用指向一个新的Integer实例。在执行methodTwo方法时,localVariable1引用将会在每一个线程中存放一份拷贝。这两个Integer对象实例化将会被存储堆上,可是每次执行这个方法时,这个方法都会建立一个新的Integer对象,两个线程执行这个方法将会建立两个不一样的Integer实例。methodTwo方法建立的Integer对象对应于上图中的Object1和Object5。

还有一点,MySharedObject类中的两个long类型的成员变量是原始类型的。由于,这些变量是成员变量,因此它们任然随着该对象存放在堆上,仅有本地变量存放在线程栈上。

硬件内存架构

现代硬件内存模型与Java内存模型有一些不一样。理解内存模型架构以及Java内存模型如何与它协同工做也是很是重要的。这部分描述了通用的硬件内存架构,下面的部分将会描述Java内存是如何与它“联手”工做的。

一个现代计算机一般由两个或者多个CPU。其中一些CPU还有多核。从这一点能够看出,在一个有两个或者多个CPU的现代计算机上同时运行多个线程是可能的。每一个CPU在某一时刻运行一个线程是没有问题的。这意味着,若是你的Java程序是多线程的,在你的Java程序中每一个CPU上一个线程可能同时(并发)执行。

每一个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操做的速度远大于在主存上执行的速度。这是由于CPU访问寄存器的速度远大于主存。

每一个CPU可能还有一个CPU缓存层。实际上,绝大多数的现代CPU都有必定大小的缓存层。CPU访问缓存层的速度快于访问主存的速度,但一般比访问内部寄存器的速度还要慢一点。一些CPU还有多层缓存,但这些对理解Java内存模型如何和内存交互不是那么重要。只要知道CPU中能够有一个缓存层就能够了。

一个计算机还包含一个主存。全部的CPU均可以访问主存。主存一般比CPU中的缓存大得多。

一般状况下,当一个CPU须要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部份内容读到它的内部寄存器中,而后在寄存器中执行操做。当CPU须要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,而后在某个时间点将值刷新回主存。

当CPU须要在缓存层存放一些东西的时候,存放在缓存中的内容一般会被刷新回主存。CPU缓存能够在某一时刻将数据局部写到它的内存中,和在某一时刻局部刷新它的内存。它不会再某一时刻读/写整个缓存。一般,在一个被称做“cache lines”的更小的内存块中缓存被更新。一个或者多个缓存行可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。

Java内存模型和硬件内存架构之间的桥接

上面已经提到,Java内存模型与硬件内存架构之间存在差别。硬件内存架构没有区分线程栈和堆。对于硬件,全部的线程栈和堆都分布在主内中。部分线程栈和堆可能有时候会出如今CPU缓存中和CPU内部的寄存器中。以下图所示:

当对象和变量存储到计算机的各个内存区域时,必然会面临一些问题,其中最主要的两个问题是:

  • 共享对象对各个线程的可见性
  • 共享对象的竞争现象

共享对象的可见性

当多个线程同时操做同一个共享对象时,若是没有合理的使用volatile和synchronization关键字,一个线程对共享对象的更新有可能致使其它线程不可见。

想象一下咱们的共享对象存储在主存,一个CPU中的线程读取主存数据到CPU缓存,而后对共享对象作了更改,但CPU缓存中的更改后的对象尚未flush到主存,此时线程对共享对象的更改对其它CPU中的线程是不可见的。最终就是每一个线程最终都会拷贝共享对象,并且拷贝的对象位于不一样的CPU缓存中。

下图展现了上面描述的过程。左边CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改成2。但这个变动对运行在右边CPU中的线程不可见,由于这个更改尚未flush到主存中: 

要解决共享对象可见性这个问题,咱们可使用java volatile关键字。 Java’s volatile keyword. volatile 关键字能够保证变量会直接从主存读取,而对变量的更新也会直接写到主存。volatile原理是基于CPU内存屏障指令实现的,后面会讲到。

竞争现象

若是多个线程共享一个对象,若是它们同时修改这个共享对象,这就产生了竞争现象。

以下图所示,线程A和线程B共享一个对象obj。假设线程A从主存读取Obj.count变量到本身的CPU缓存,同时,线程B也读取了Obj.count变量到它的CPU缓存,而且这两个线程都对Obj.count作了加1操做。此时,Obj.count加1操做被执行了两次,不过都在不一样的CPU缓存中。

若是这两个加1操做是串行执行的,那么Obj.count变量便会在原始值上加2,最终主存中的Obj.count的值会是3。然而下图中两个加1操做是并行的,不论是线程A仍是线程B先flush计算结果到主存,最终主存中的Obj.count只会增长1次变成2,尽管一共有两次加1操做。 

要解决上面的问题咱们可使用java synchronized代码块。synchronized代码块能够保证同一个时刻只能有一个线程进入代码竞争区,synchronized代码块也能保证代码块中全部变量都将会从主存中读,当线程退出代码块时,对全部变量的更新将会flush到主存,无论这些变量是否是volatile类型的。

参考资料:

  http://ifeve.com/java-memory-model-6/

  http://www.infoq.com/cn/articles/java-memory-model-1

相关文章
相关标签/搜索