在并发编程中,咱们须要处理两个问题,线程之间如何通讯以及线程之间如何同步java
通讯是指线程之间依靠何种机制交换信息。在命令式编程中,线程之间通讯主要依靠共享内存和消息机制来进行通讯。程序员
在共享模型机制中,线程之间共享程序的公共状态,线程之间经过写-读内存中的公共状态来进行隐式的通讯,在消息传递的并发模型里,线程之间没有公共状态,线程之间必须经过明确的消息来进行显示的通讯。编程
同步是程序用于控制不一样线程之间操做发生相对顺序的机制。在共享内存并发模型里面,同事是显示进行的。程序员必须显示的指定方法或者某段代码段须要在线程之间互斥执行。在消息传递模型中,由于接收消息必须在发送消息以后,因此他们之间的同步是隐式操做数组
java采起的是同步共享并发模型,java线程之间的通讯老是隐式进行,对程序员来讲是透明的。缓存
java内存模型:并发
在jvm内存中,定了对象的实例,数组,静态域是存放在堆内存中的,堆内存这块就是线程共享的。虚拟机栈、本地方法栈和程序计数器属于线程私有的,不存在并发的问题,由于不存在内存可见性的问题,也不瘦内存模型的影响。app
java线程通讯是有java内存模型控制,也就是JMM,JMM决定了一个线程对共享变量的写入操做什么时候对另外一个线程可见。从抽象的角度讲,JMM决定了线程与主内存之间的抽象关系,线程之间的共享变量什么时候写入主内存中,每一个线程都存在一个私有的本地内存,本地内存存储了该线程以读/写操做共享变量的副本,这里所说的本地内存也是一种抽象的概念,它涵盖了缓存,写缓冲区,寄存器以及其余硬件。JAVA内存模型抽象图以下:jvm
从上图能够看出若是线程A要和线程B进行通讯的话,姚经理两个步骤:高并发
1:首先线程A要把本地变量的副本刷新到主内存中spa
2:而后,线程B去主内存中读取线程A以前更新的变量
下面是一个示意图:
如上图所示,线程A和线程B都有主内存中X变量的副本,线程A在执行的时候,首先把主内存中的变量x load到本地内存中,当线程A须要和线程B进行通讯的时候,线程A再把本地内存中已经更新了的x的值store到主内存中,线程B再从主内存中读取共享变量x的值到本地内存B之中,而后对线程B来讲x的值就会更新为1。
从总体来看,其实就至关于线程A给线程B发消息,这个通讯过程依靠主内存,JMM控制主内存和本地内存进行交互。
重排序:
在执行程序的时候,编译器和处理器须要对指令进行重排序。重排序包括三种
1:编译器的重排序。编译器在不改变单线程语义的状况下,能够安排语句的执行顺序,好比
int a = 10; 1
int b = 10; 2
int c = a * b; 3
可能里面的执行顺序就2在一以前,它在保证输出结果正确的前提下容许指令进行从新排序
2:指令级并行的重排序,现代处理器采用了指令级并行技术来将多条指令并行执行。若是不存在数据依赖,那么处理器也能够改变语句对应机器指令的执行顺序
3:内存系统重排序,因为处理器使用缓存进行读写操做,这使得加载和存储看上去是乱序的,从java源代码到最终执行的二级制代码,会经历一下三种排序
第一级别的重排序属于编译器级别的重排序,第二和第三级别属于处理器级别的重排序,对于编译器,JMM的编译器重排序规则会禁止特定的编译重排序(不是全部的编译器都要重排序),对弈处理器级别的重排序,JMM则要求java编译器在生成指令序列的时候,插入特定类型的内存屏障指令,经过内存屏障指令来禁止特定类型的处理器重排序(不是全部的处理器重排序)。
JMM属于语言级别的内存模型,他保证了在不一样的编译器和不一样的处理器平台上,经过特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
处理器重排序与内存屏障指令
现代的处理器都经过写缓冲区的方式临时保存而后在向内存中写入数据。写缓冲区的操做保证了指令流水线运行,同事他能够避免处理器停顿下来等待向内存中写入数据产生的延迟。同事能够经过批处理的方式刷新缓冲区,或者合并缓冲区内对同一地址的屡次修改,能够减小内存总线的占用。虽然写缓冲区有不少好处,可是每一个处理器的写缓冲区只对该处理器可见。这就会对内存的操做顺序产生重要的影响:处理器对内存的读/写操做的执行顺序,不必定与内存实际发生的读/写顺序一致,为了具体说明,请看一下实例:
ProcessorA | ProcessorB |
a=1 //A1 | b=2 //B1 |
x=b //A2 | y=a //B2 |
初始状态是a=b=0
处理器容许执行后获得的结果是x=y=0
假设程序器A和处理器B按照程序的顺序访问执行内存操做,最终却可能获得x=y=0的结果。具体缘由以下:
在这里处理器A和处理器B同时把共享变量写入本身的写缓冲区内,而后从内存中读取以前的共享变量数据,而后再把本身的写缓冲区内的数据刷新到内存中,那么就有可能出现a=b=0的状况,实际上这种状况下,处理器A和处理器B读到的数据就是脏数据。
从内存的执行顺序来看,知道处理器A完成了A3,将数据刷新到内存中,才算是完成了A1这个写操做,虽然处理器A执行的操做顺序A1>A2,可是在内存中却变成了A2>A1,此时,处理器A的操做顺序就被重排序了,处理器B也是同样
这里的关键地方就是,写缓冲区仅对本身可见,他会致使处理器执行内存操做的殊勋可能会与内存实际操做顺序不一致。因为如今的处理器都作写缓冲区,所以如今处理器都会容许对写-读操做进行重排序。
下面是常见处理器容许重排序类型的列表:
Load-Load | Load-Store | Store-Store | Store-Load | 数据依赖 | |
sparc-TSO | N | N | N | Y | N |
x86 | N | N | N | Y | N |
ia64 | Y | Y | Y | Y | N |
PowerPC | Y | Y | Y | Y | N |
以上表格N表示不容许,Y表示容许
sparc-TSO是指以TSO(total store order)内存模型运行时,sparc处理器特性
x86包括x64和AMD64
为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障分为如下四个类型:
屏障类型 | 指令实例 | 说明 |
LoadLoad Barriers | Load1; LoadLoad; Load; |
确保Load1数据的装载,以前与Load2以及后续全部指令的装载 |
StoreStore Barriers | Store1; StoreStore; Store2; |
确保Store1的数据对其余处理器可见(从写缓冲区刷新到内存), 以前与Store2以及后续全部指令的操做 |
LoadStore Barriers | Load1; LoadStore; Store2; |
确保Load1数据的装载,以前与Store2以及后续全部指令的操做 |
StoreLoad Barriers | Store1; StoreLoad; Load2; |
确保Store1数据对其余处理器可见(从写缓冲区刷新到内存), 以前与Load2及后续全部指令的装载。 StoreLoad Barries会使该屏障以前的全部内存指令(Store和Load) 完成以后,才执行屏障以后的内存访问指令。 |
StoreLoad Barries是一个全能型的内存屏障指令,他同时具备其余三个屏障的效果,现代处理器大都支持该屏障,可是该屏障的开销会很昂贵,由于当前处理器会把写缓冲区内的数据所有刷新到内存中去。
happens-before
从jdk1.5开始,java使用最新的JSR-133内存模型。
JSR-133内存模型提出了happens-before的概念,经过这个概念阐述了操做之间的内存可见性。若是一个操做执行结果须要对另外一个操做可见,那么两个操做之间必须存在happen-before原则,这里的两个操做,既能够是同一个线程之间的,也能够是不一样线程之间的。和咱们平常开发关系密切相关的happens-before原则:
1:程序顺序原则:一个线程的每一个操做,happens-before与线程中的任意后续操做
2:监视器锁规则:对一个监视器锁的解锁,happens-before与随后队这个监视器锁的加锁操做
3:volatile变量规则:对一个volatile域的写,happens-before与后续对这个volatile域的读操做
4:传递性:若是A操做happens-before与B操做,B操做happens-before与C操做,那么A操做happens-before与C操做
须要注意的一点就是两个操做有happens-before规则,可是并不意味着前一个操做必须在后一个操做执行完以后再去执行,这里happens-before原则仅仅是要求前一个操做的结果必须对后一个操做的结果可见,切前一个程序的操做顺序排在第二个以前
好比:
在单线程操做中
double a = 23.32; //A
double b = 23.43; //B
double c = a*b; //C
根据程序的happens-before原则,
A happens-before B
B happens-before C
A happens-before C
那么是否是就要A必定要在B执行前执行,其实否则,B有可能在A执行前执行。JMM仅仅要求前一个操做的结果要对后一个操做的结果可见,这里操做A的结果其实不须要对操做B可见,并且重排序操做A和操做B以后的执行结果与A happens-before B顺序执行的结果一致,这种状况下JMM认为重排序并不非法,JMM容许这样的重排序。
上述所说的单线程操做其实就是JMM中的as-if-serial语义
as-if-serial语义的意思是指:无论怎么排序(编译器和处理器为了提升并发的速度),(单线程)程序执行结果不会改变,编译器,runtime,和处理器都必须遵循as-if-serial语义
为了遵循as-if-serial语义,编译器和处理器不会对存在有数据依赖关系的的操做进行重排序,由于这种重排序会改变程序的操做结果。可是不存在依赖关系的操做可能会被重排序,上面单线程例子就是。