在多线程编程中,须要处理两个最核心的问题,线程之间如何通讯及线程之间如何同步,线程之间通讯指的是线程之间经过何种机制交换信息,同步指的是如何控制不一样线程之间操做发生的相对顺序。不少读者可能会说这还不简单,java中的同步采用的是锁机制或volatile来完成的,的确,在应用层,java中的同步的确是经过加锁来完成的,可是锁机制是如何实现的呢?这就涉及到java中的内存模型的相关知识。本博客将带领你们了解java内存模型的相关知识。java
若是读者以为本博客写的不错,记得小手一抖,点个赞哦!另外欢迎你们关注个人博客帐号哦,将会不按期的为你们分享技术干货,福利多多哦!程序员
咱们知道java中多线程通讯采用的是共享内存模型,即多个线程之间共享某块内存,经过写-读内存中的公共状态进行隐式通讯,整个通讯过程对于程序员彻底透明,所以理解java内存模型将帮助咱们理解这种隐式通讯的原理,从而更好的写出java多线程程序。编程
一java内存模型的抽象结构:数组
咱们知道在java中,对象实例域,静态域和数组元素存储在堆内存中,堆内存在线程之间共享,咱们称对象实例域,静态域和数组元素为共享变量,而局部变量,方法定义的参数和异常处理器参数不会在线程之间共享,它们不存在内存可见性的问题,所以不受java内存模型的影响。缓存
java线程之间的通讯受java内存模型(Java Memory Model,简称JMM)的控制,JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见,从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,而每一个线程各自拥有属于本身的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。注意本地内存是一个抽象概念,在物理设备上不存在,它一般包含缓存,写缓冲区,寄存器以及其余的硬件和编译器优化等。java内存模型的抽象示意图以下:多线程
从图可知,线程A与线程B之间如要通讯的话,必需要经历下面2个步骤:性能
1首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2而后,线程B到主内存中去读取线程A以前已更新过的共享变量。
优化
即java线程之间的通讯必须通过主内存,JMM经过经过控制主内存与每一个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。spa
二指令序列的重排序:线程
前面说过每一个线程拥有本身的本地内存(一个抽象的概念,多个物理设备内存的抽象),其中一种就是硬件和编译器优化。在执行程序时,为了提升性能,编译器和处理器一般会对指令作重排序,之因此把这个拿出来说,是由于咱们知道CPU将按照指令序列执行指令,若是指令被重排序,那么对线程的读写会产生影响,这就会影响咱们前面提到的java内存模型。因此接下来就介绍一下重排序,重排序包括3种类型
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。若是不存在数据依赖性,处理器能够改变语句对 应机器指令的执行顺序。
3)内存系统的重排序。因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是在乱序执行
其中第一种很好理解,这是保证程序顺序执行最基本的原则,第二条中的若是数据不存在依赖关系这点给你们解释一下,示例代码以下:
int x=1; int y=x+1; int z=1;
int z=1; int x=1; int y=x+1;但不能为:
int z=1; int y=x+1; int x=1;//x赋值必须在y以前上述3种重排序可能会致使多线程程序出现内存可见性问题,对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序,对于处理器,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,经过内存屏障指令来禁止特定类型的处理器重排序
三java内存模型内存屏障指令
前面说过,常见的处理器都会对程序指令进行重排序,而这在多线程中极可能致使内存可见性问题,而java内存模型确保在不一样的编译器和不一样的处理器平台之上,经过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。这也是java内存模型根本做用。而禁止重排序的方法就是插入内存屏障指令,为了更好的理解为什么须要禁止重排序,咱们先来看一个例子:
假设处理器A和处理器B按程序的顺序并行执行内存访问,最终却可能获得x = y = 0的结果。具体的缘由以下图所示:
这里对这个图稍做一下解释,由于写缓冲区仅对本身的处理器可见,因此虽然处理器A已经在缓冲区A中更新了a的值,可是处理器B不能感知到,所以处理器B从内存中读取a的值赋给y时,若是此时处理器A还未将a的值刷新到内存中,那么此时内存中a的值仍然为0,这样y的值就为0,同理x的值可能为0,而这显然不是咱们所指望的结果,
之因此出现上述结果是由于现代的处理器都会使用写缓冲区来临时保存向内存写入的数据,这相信你们在计算机组成原理这么课中都学过,但每一个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操做的执行顺序产生重要的影响:处理器对内存的读/写操做的执行顺序,不必定与内存实际发生的读/写操做顺序一致
咱们知道对内存的操做包括读-写两种,那么多线程访问同一个共享变量则两两组合共四种状况,现代常见处理器的重排序对这四种组合容许状况以下所示:
上图中“N”表示处理器不容许两个操做重排序,“Y”表示容许重排序。咱们能够看出:常见的处理器都容许Store-Load重排序;常见的处理器都不容许对存在数据依赖的操做作重排序。(注意上图所说的x86包括x64及AMD64。)
与上图对应java内存模型定义了四种禁止重排序的四种指令屏障,以下图所示:
java内存模型经过这四种内存屏障指令来保证了前面咱们所举的例子的状况不会出现,仍然以上述例子来讲明,Java内存模型经过在适当位置插入内存屏障指令,如StoreLoad Barriers指令,则能够保证Store1数据对其余处理器是可见的(即将缓存中的内容刷新到内存),这样在处理器A将a的值=1写入缓冲区A后将及时保证处理器B在从内存中读取a的值以前会将处理器A缓存中的值刷新到内存中。从而保证内存可见性。
注意:StoreLoad Barriers是一个“全能型”的屏障,它同时具备其余三个屏障的效果。现代的多处理器大都支持该屏障(其余类型的屏障不必定被全部处理器支持)。执行该屏障开销会很昂贵,由于当前处理器一般要把写缓冲区中的数据所有刷新到内存中(buffer fully flush)。
以上就是本博客的主要内容,java内存模型主要解决多线程程序中的内存可见性问题,该内容是理解java多线程编程的理论基础。
若是读者以为本博客写的不错,记得小手一抖,点个赞哦!另外欢迎你们关注个人博客帐号哦,将会不按期的为你们分享技术干货,福利多多哦!