内存模型同并发息息相关,熟悉内存模型将对虚拟机、多线程及线程安全问题有更深刻的了解。程序员
1.什么是内存模型?编程
给出定义以前,让咱们先来了解一下物理计算机中的并发问题。咱们都知道,处理器运行时必然要和内存交互,并且这个I/O操做是很难消除的,但因为计算机存储设备和处理器的运算速度有几个数量级的差距,因此在二者之间加入了一层读写速度尽量接近处理器运算速度的高速缓存,这样处理器就不用等待缓慢的内存读写了。可是这样引出了一个新的问题:缓存一致性。如图:数组
当多个处理器运算任务都涉及同一块主内存区域时,将可能致使各自的缓存数据不一致。为了解决一致性问题,须要各个处理器访问缓存时都要遵照一些协议,在读写时要根据协议来操做。因此,内存模型能够理解为:在特定的操做协议下,对特定的内存或高速缓存进行读写访问的过程抽象。缓存
2.什么是Java内存模型?安全
Java内存模型即JMM(Java Memory Model),它的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节,这里的变量指的是共享变量(如实例字段、静态字段和构成数组对象的元素,不包括局部变量与方法参数,由于是线程私有);在并发编程中,JMM决定一个线程对共享变量的写入什么时候对另一个线程可见。JMM规定了全部的变量都存储在主内存中,每一个线程都有本身的工做内存,线程对变量的全部操做都必须在工做内存中进行。多线程
注:此处主内存和结构中的堆、栈等是不一样层次上的划分,二者基本没有关系;若是硬要扯上关系,能够理解为主内存主要对应于Java堆中对象实例数据部分,而工做内存则对应于虚拟机栈中的部分区域。从更低层次上说,主内存直接对应于物理内存,为了获取更好的运行速度,虚拟机可能会让工做内存优先存储于寄存器和高速缓存中,由于程序运行时主要访问读写的是工做内存。并发
3.内存间的交互操做:app
关于主内存和工做内存之间具体的交互协议,即一个变量如何从主内存拷贝到工做内存、如何从工做内存同步回主内存之类的实现细节,Java内存模型定义了8种操做来完成:函数
若是要把一个变量从主内存复制到工做内存,那就要顺序地执行read和load操做,反之,就要顺序地执行store和write操做。注:Java内存模型只要求上述两个操做必须顺序执行,没有说必须连续执行。除此以外,JMM还规定了在执行上述8种基本操做时必须知足以下规则:性能
(1).不容许read和load、store和write操做之一单独出现。
(2).不容许一个线程丢弃它最近的assign操做,即变量在工做内存中改变了以后,必须把该变化同步回主内存。
(3).不容许一个线程无缘由地把数据从工做内存同步回主内存。
(4).一个新的变量只能诞生在主内存中,即use、store以前必定要先assign、load。
(5).一个变量同一时刻只容许一条线程对其lock,但lock操做能够被同一线程重复屡次,而后只有执行一样次数的unlock才会被解锁。
(6).若是对一个变量执行lock操做,将会清空工做内存中此变量的值,在执行引擎使用这个变量前,需从新load或assign,初始化变量的值。
(7).若是一个变量事先没有被lock,那将不被容许unlock,也不容许去unlock一个被其余线程锁定住的变量。
(8).对一个变量unlock前,必须把变量同步回主内存中。
注:JMM要求这8个操做都具备原子性,但对于64位数据的long和double类型来讲,容许划分为两次的32位的操做来进行。多线程环境下,理论上可能有取到半个变量值的可能性,不过不用担忧,目前商用Java虚拟机容许把这些操做视为原子性操做,因此不用担忧这种状况的出现。
总结:以上就是处理器、工做内存、主内存之间变量交互的操做;边读边理解,脑海里有一副交互图,再来读这几个命令,就容易记住了。
4.JMM--并发编程模型的两个问题
上面咱们清楚了共享变量读出内存和写入内存的交互操做是怎样的一个流程,把它想象成单线程的一条线的操做,就比较好理解。BUT,咱们想过没有,若是是并发环境下呢?会产生什么问题?
问题(1):线程之间如何通讯? 问题(2).线程之间如何同步?
在命令式编程中,线程通讯机制有两种:共享内存和消息传递。Java的并发采用的是共享内存模型,在此模型里,线程之间共享程序的公共状态,经过写-读内存中的公共状态进行隐式通讯,整个过程对程序员是彻底透明的;同步是指程序中用于控制不一样线程间操做发生相对顺序的机制,在共享模型里,同步是显式进行的,即程序员必须显式指定某个方法或某段代码须要在线程之间互斥执行。
5.JMM--抽象结构
从抽象角度看,JMM定义了线程和主内存之间的抽象关系:每一个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其余的硬件和编译器优化。
从图上来看,若是线程A和线程B要进行通讯,必须经历两个步骤:
(1).线程A把本地内存A中更新过的共享变量刷新到主内存中。
(2).线程B到主内存中读取线程A已经更新过的共享变量。
从总体上看,实质是线程A向线程B发送消息,JMM就是控制主内存与每一个线程本地内存间的交互,来提供内存可见性保证。
6.JMM--原子性、可见性、有序性
介绍完JMM的相关操做和规则,再来总结一下JMM的特征。JMM是围绕着在并发中如何处理原子性、可见性和有序性这三个特征来创建的。
原子性:由Java内存模型来直接保证的原子性变量操做包括read、load、assign、use、store和write,咱们大体能够认为基本数据类型的访问读写是具有原子性的(64位的long和double的非原子协议,知道就可,几乎不会发生)。如须要更大范围的原子性保证,可用同步块--synchronized关键字。
可见性:是指当一个线程修改了共享变量的值,其余线程能够当即得知这个修改。volatile关键字能够保证多线程操做时变量的可见性,而普通变量不能。除此以外,synchronized和final也保证了可见性。
有序性:Java程序中自然的有序性能够总结为一句话:“若是在本线程内观察,全部的操做都是有序的;若是一个线程观察另外一个线程,全部的操做都是无序的”。前半句指“线程内表现为串行的语义”,后半句指“指令重排序现象”和“工做内存和主内存同步延迟现象”。Java提供两个关键字来保证线程间操做的有序性:volatile和synchronized。前者自己就包含了指令重排序的语义;后者则是由“一个变量在同一时刻只容许一条线程对其进行lock操做”这条规则来实现,决定了持有同一个锁的两个同步块只能串行执行。
7.happens-before(先行发生原则)
Java语言中有一个“先行发生”原则,这个原则很是重要,它是判断数据是否存在竞争、线程是否安全的主要依据。那happens-before指什么呢?让咱们来看一下:
happens-before是Java内存模型中定义的两项操做之间的偏序关系,即操做A发生在操做B以前,操做A产生的影响内被操做B观察到,“影响”包括改变共享变变量值、发送消息、调用方法等。让咱们看下示例:
若是A操做先与操做B发生,变量j必定等于1,缘由:根据happens-before原则,A的改变能够被B观察到;C尚未被执行。如今来考虑C操做,A仍是先于B发生,但C出如今A和B中间,可是C和B没有先行发生关系,那j会是多少?1仍是2?答案不肯定,由于C操做对i的改变,可能会被B观察到,也可能不会,因此不具有线程安全性。Java内存模型存在一些自然的happens-before关系:
程序次序规则:在一个线程内,按照代码程序顺序,书写在前面的操做先发生于书写在后面的操做。准确的说,应该是控制流顺序而不是代码顺序,由于要考虑分支、循环等结构。
管程锁定规则:一个unlock操做先行发生于后面对同一个锁的lock操做。“后面”指时间上的前后顺序。
volatile变量规则:对一个volatile变量的写操做先发生于后面对这个变量的读操做。
线程启动规则:Thread对象的start()方法先行发生于此线程的每个动做。“后面”指时间上的前后顺序。
线程终止规则:线程中全部操做都先发生于此线程的终止检测。能够经过Thread.join()方法结束、Thread.isAlive()的返回值等检测线程是否终止。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
对象终结规则:一个对象初始化完成(构造函数执行完毕)先行发生于它的finalized()方法的开始。
传递性:A操做先行发生于B操做,B操做先于C,则A先于C。
那如何根据这些规则来判断操做间是否具备顺序性?对于读写共享变量的操做来讲,就是是否线程安全?请看以下示例:
这是一组普通的getter/setter方法,假设存在线程A和线程B,线程A先(时间上的前后)调用了setValue(1),而后线程B调用了同一个对象的getValue(),那线程B收的返回值是什么?
咱们发现没有一个规则与之匹配,因此返回结果是不肯定的,故线程不安全。解决办法:把getter/setter方法都定义为synchronized方法,套用管程锁定规则;定义为volatile变量,因为setter方法的修改不依赖value原值,符合只用场景。由此咱们得出告终论:一个操做“时间上的先发生”不表明这个操做会先行发生。一样,反之亦不成立(指令重排序)。一句话就是时间前后顺与先行发生基本没有太大关系,一切以先行发生原则为准。
8.重排序
在执行程序时,为了提升性能,编译器和处理器经常会对指令作重排序。重排序分三种类型:
(1).编译器优化的重排序。编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序。
(2).指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。若是不存在数据依赖性,处理器能够改变语句对应的机器指令的执行顺序。
(3).内存系统的重排序。因为处理器使用缓存和读/写缓冲区,这是的加载和存储看上去多是在乱序中执行。
●数据依赖性:编译器和处理器在重排序时,会遵照数据依赖性,不会改变存在数据依赖关系的两个操做的执行顺序。这里只对单个处理器的指令序列和单个线程中执行的操做有效。
●as-if-serial:无论怎么重排序(编译器和处理器为了提升并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵照as-if-serial语义。as-if-serial语义把单线程保护了起来,便形成了一个幻觉:单线程程序按程序的顺序来执行的。
●程序顺序规则:
double pi = 3.14; // A A happens-before B
double r = 1.0; // B ====》对应三个happens-before关系: B happens-before C
double area = pi * r * r; // C A happens-before C
这里A happens-before B,但实际执行时B却能够在A以前执行。JVM不要求A必定要先于B执行,仅仅要求A的操做结果对B可见,而且A的操做顺序先于B操做。这里A不须要对B可见,且重排的结果(BAC)与以前(ABC)一致,则JMM认为这种排序并不非法,并容许。
●重排序对多线程的影响:在多线程中,对存在控制依赖的操做重排序,可能会改变程序的执行结果。
8.synchronized实现可见性
JMM关于synchronized的两条规定:
(1)线程解锁前,必须把共享变量的最新值刷新到主内存中
(2)线程加锁时,清空工做内存中共享变量的值,从而须要共享变量时须要从主内存中读取最新的值。
注意:加锁与解锁须要是同一把锁,线程解锁前对共享变量的修改对其余线程不可见。
咱们先假设几种状况:执行顺序为1.1->1.2->1.3->1.4,执行结果为6.过程:先执行write()方法,变量的改变可以及时写入主内存,而后执行ready()方法,能够在主内存读取到最新的变量值;1.1->2.1->2.2->1.2,result值为3;1.2->2.1->2.2->1.1,result值为0
致使共享变量在线程间不可见的缘由:(1)线程的交叉执行 (2)重排序结合线程交叉执行 (3)共享变量更新后的值没有在工做内存与主内存及时更新
解决办法:在保证写线程先执行的前提下,用write、ready方法用synchronized修饰。首先阻止了线程的交叉执行,其次单线程的重排序不影响结果,最后对变量的改变可见。
9.volatile实现可见性
volatile关键字:保证变量的可见性,不能保证变量复合操做的原子性。
如何保证可见性?深刻来讲:经过加入内存屏障和禁止重排序优化来实现的。
●对volatile变量执行写操做时,会在写操做后加入一条store屏障指令,它会把写缓存强制刷新到主内存中去,这样主内存中就是变量的最新值,同时防止处理器把volatile前面的变量重排序到写变量以后。
●对volatile变量执行读操做时,会在写操做后加入一条load屏障指令,它会使缓存区的变量失效。