转自:http://blog.csdn.net/suifeng3051/article/details/52611310java
Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工做方式。JVM是整个计算机虚拟模型,因此JMM是隶属于JVM的。程序员
若是咱们要想深刻了解Java并发编程,就要先理解好Java内存模型。Java内存模型定义了多线程之间共享变量的可见性以及如何在须要的时候对共享变量进行同步。原始的Java内存模型效率并非很理想,所以Java1.5版本对其进行了重构,如今的Java8仍沿用了Java1.5的版本。编程
在并发编程领域,有两个关键问题:线程之间的通讯和同步。缓存
线程的通讯是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通讯机制有两种共享内存和消息传递。多线程
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间经过写-读内存中的公共状态来隐式进行通讯,典型的共享内存通讯方式就是经过共享对象进行通讯。架构
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须经过明确的发送消息来显式进行通讯,在java中典型的消息传递方式就是wait()和notify()。并发
关于Java线程之间的通讯,能够参考线程之间的通讯(thread signal)。app
同步是指程序用于控制不一样线程之间操做发生相对顺序的机制。jvm
在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码须要在线程之间互斥执行。性能
在消息传递的并发模型里,因为消息的发送必须在消息的接收以前,所以同步是隐式进行的。
Java线程之间的通讯老是隐式进行,整个通讯过程对程序员彻底透明。若是编写多线程程序的Java程序员不理解隐式进行的线程之间通讯的工做机制,极可能会遇到各类奇怪的内存可见性问题。
上面讲到了Java线程之间的通讯采用的是过共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每一个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化。
从上图来看,线程A与线程B之间如要通讯的话,必需要经历下面2个步骤:
1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。 2. 而后,线程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内存模型工做方式,下面就JVM对Java内存模型的实现、硬件内存模型及它们之间的桥接作详细介绍。
在JVM内部,Java内存模型把内存分红了两部分:线程栈区和堆区,下图展现了Java内存模型在JVM中的逻辑视图:
JVM中运行的每一个线程都拥有本身的线程栈,线程栈包含了当前线程执行的方法调用相关信息,咱们也把它称做调用栈。随着代码的不断执行,调用栈会不断变化。
线程栈还包含了当前方法的全部本地变量信息。一个线程只能读取本身的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。即便两个线程执行的是同一段代码,它们也会各自在本身的线程栈中建立本地变量,所以,每一个线程中的本地变量都会有本身的版本。
全部原始类型(boolean,byte,short,char,int,long,float,double)的本地变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于原始类型的本地变量,一个线程能够传递一个副本给另外一个线程,当它们之间是没法共享的。
堆区包含了Java应用建立的全部对象信息,无论对象是哪一个线程建立的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。无论对象是属于一个成员变量仍是方法中的本地变量,它都会被存储在堆区。
下图展现了调用栈和本地变量都存储在栈区,对象都存储在堆区:
一个本地变量若是是原始类型,那么它会被彻底存储到栈区。
一个本地变量也有多是一个对象的引用,这种状况下,这个本地引用会被存储到栈中,可是对象自己仍然存储在堆区。
对于一个对象的成员方法,这些方法中包含本地变量,仍须要存储在栈区,即便它们所属的对象在堆区。
对于一个对象的成员变量,无论它是原始类型仍是包装类型,都会被存储到堆区。
Static类型的变量以及类自己相关信息都会随着类自己存储在堆区。
堆中的对象能够被多线程共享。若是一个线程得到一个对象的应用,它即可访问这个对象的成员变量。若是两个线程同时调用了同一个对象的同一个方法,那么这两个线程即可同时访问这个对象的成员变量,可是对于本地变量,每一个线程都会拷贝一份到本身的线程栈中。
下图展现了上面描述的过程:
不论是什么内存模型,最终仍是运行在计算机硬件上的,因此咱们有必要了解计算机硬件内存架构,下图就简单描述了当代计算机硬件内存架构:
现代计算机通常都有2个以上CPU,并且每一个CPU还有可能包含多个核心。所以,若是咱们的应用是多线程的话,这些线程可能会在各个CPU核心中并行运行。
在CPU内部有一组CPU寄存器,也就是CPU的储存器。CPU操做寄存器的速度要比操做计算机主存快的多。在主存和CPU寄存器之间还存在一个CPU缓存,CPU操做CPU缓存的速度快于主存但慢于CPU寄存器。某些CPU可能有多个缓存层(一级缓存和二级缓存)。计算机的主存也称做RAM,全部的CPU都可以访问主存,并且主存比上面提到的缓存和寄存器大不少。
当一个CPU须要访问主存时,会先读取一部分主存数据到CPU缓存,进而在读取CPU缓存到寄存器。当CPU须要写数据到主存时,一样会先flush寄存器到CPU缓存,而后再在某些节点把缓存数据flush到主存。
正如上面讲到的,Java内存模型和硬件内存架构并不一致。硬件内存架构中并无区分栈和堆,从硬件上看,不论是栈仍是堆,大部分数据都会存到主存中,固然一部分栈和堆的数据也有可能会存到CPU寄存器中,以下图所示,Java内存模型和计算机硬件内存架构是一个交叉关系:
当对象和变量存储到计算机的各个内存区域时,必然会面临一些问题,其中最主要的两个问题是:
1. 共享对象对各个线程的可见性 2. 共享对象的竞争现象
当多个线程同时操做同一个共享对象时,若是没有合理的使用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类型的。
在执行程序时,为了提升性能,编译器和处理器会对指令作重排序。可是,JMM确保在不一样的编译器和不一样的处理器平台之上,经过插入特定类型的Memory Barrier
来禁止特定类型的编译器重排序和处理器重排序,为上层提供一致的内存可见性保证。
若是两个操做访问同一个变量,其中一个为写操做,此时这两个操做之间存在数据依赖性。
编译器和处理器不会改变存在数据依赖性关系的两个操做的执行顺序,即不会重排序。
无论怎么重排序,单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵照as-if-serial语义。
上面讲到了,经过内存屏障能够禁止特定类型处理器的重排序,从而让程序按咱们预想的流程去执行。内存屏障,又称内存栅栏,是一个CPU指令,基本上它是一条这样的指令:
编译器和CPU可以重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:无论什么指令都不能和这条Memory Barrier指令重排序。
Memory Barrier所作的另一件事是强制刷出各类CPU cache,如一个Write-Barrier
(写入屏障)将刷出全部在Barrier以前写入 cache 的数据,所以,任何CPU上的线程都能读取到这些数据的最新版本。
这和java有什么关系?上面java内存模型中讲到的volatile是基于Memory Barrier实现的。
若是一个变量是volatile
修饰的,JMM会在写入这个字段以后插进一个Write-Barrier
指令,并在读这个字段以前插入一个Read-Barrier
指令。这意味着,若是写入一个volatile
变量,就能够保证:
从jdk5开始,java使用新的JSR-133内存模型,基于happens-before的概念来阐述操做之间的内存可见性。
在JMM中,若是一个操做的执行结果须要对另外一个操做可见,那么这两个操做之间必需要存在happens-before关系,这个的两个操做既能够在同一个线程,也能够在不一样的两个线程中。
与程序员密切相关的happens-before规则以下:
注意:两个操做之间具备happens-before关系,并不意味前一个操做必需要在后一个操做以前执行!仅仅要求前一个操做的执行结果,对于后一个操做是可见的,且前一个操做按顺序排在后一个操做以前。
参考文档 :
1. http://www.infoq.com/cn/articles/java-memory-model-1
2. http://www.jianshu.com/p/d3fda02d4cae