Java内存模型基础

并发编程模型的两个关键问题

在编发编程里,需要处理的两个关键性问题:线程之间如何通信、线程之间如何同步。通信是指宣称之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
在共享内存的模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。在消息传递的并发模型里,线程之间必须通过发送消息来显式进行通信

同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模型里,同步是显示进行的。程序员必须显示指定某个方法或某段代码需要在线程之间互斥。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式的。

Java的并发采用的共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

Java内存模型的抽象结构

在Java中,所有实例域、静态域和数组元素都存储在堆中,堆内存在线程之间共享。局部变量(Local Variables)、方法定义参数和异常处理器不会在线程之间共享,不会有内存可见性问题,也不受内存模型的影响。

Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程堆共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM决定了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每一个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM一个抽象概念,并不真实存在。
在这里插入图片描述
JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证

从源代码到指令序列重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分为3类

  1. 编译器优化的重排序:编译器在不改变单线程语义的前提下可以重新安排语义的执行顺序
  2. 指令级并行的重排序:现在处理器采用了指令级并行技术来将多太哦指令重叠执行。如果不存在数据以来性,处理器可以改变语句的对应机器指令的执行顺序。
  3. 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行的。
    (2、3叫做处理器重排序)
    在这里插入图片描述

JMM属于语言级别的内存模型,它确保在不同的编译器和不同的处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序,来提供一致的内存可见性保证

并发编程模型分类

写缓冲区:现在处理器使用写缓冲区临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,减少对内存总线的占用
但是每个处理器上的写缓冲区只会对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序不一定与内存实际发生的读/写操作顺序一致

为了保证内存可见性,Java编译器在生成指令序列的适当为止会插入内存屏障指令来禁止特定类型的处理器重排序。
JMM把内存屏障分为4类:

屏障类型指令示例说明
LoadLoad BarriersLoad1;LoadLoad;Load2确保Load1数据的装载先于Load2及所有后续装载指令的装载
StoreStore BarriersStore1;StoreStore;Store2确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有的后续存储指令的存储
LoadStore BarriersLoad1;LoadStore;Store2确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存
StoreLoad BarriersStore1;StoreLoad;Load2确保Store1数据对其他处理器变得可见(刷新到内存)先于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令

SotreLoad Barriers是一个全能型屏障,它同时具备其他三个屏障的效果。现在处理器大多支持该屏障(其他不一定被锁哟u 处理器支持)。执行屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。

happens-before简介

从Java 5开始,Java使用新的JSR-133内存模式。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这里两个操作之间必须要存在happens-before关系。这里提到的两个操作即可以是同一个线程之内,也可以是在不同线程之内。

与程序员密切相关的happens-before规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中得任意后续操作。
  • 监视器锁规则:对一个锁的解锁,hanppens-before于随后对这个锁的加锁
  • volatile变量规则:对一个volatile域的写,hanppens-before于任意后续对这个volatile域的读
  • 传递性:如果A happens-before B,B happens-before C,那么 A hanppens-before c 。

注意:两个操作之间具有hanppens-before关系,并不意味着前一个操作必须要在猴哥操作之前执行!hanppens-before仅仅要求前一个操作(执行结果)对后一个操作可见,且前一个操作按照顺序排在第二个操作之前。
在这里插入图片描述
一个hanppens-before规则对应于一个或多个编译器和处理器重排序规则。对于Java程序员来说,hanpens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现线方法。

重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重排序的一种手段

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性(仅针对单个处理器中执行的指令序列和单个线程中执行的操作)
编译器和处理器在重排序时,会遵守数据以来性,编译器和处理器不会改变存在数据依赖关系的连个操作的执行顺序。

as-if-serial语义

as-if-serial的语义是:不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义
as-if-serial语义把单线程程序保护起来,遵守as-if-serial语义的编译器、runtime和处理器共同创造一个程序是按照顺序执行的幻觉。as-if-serial语义使单线程程序猿无需担心重排序会干扰他们,也无需担心内存可见性问题。
但是如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

重排序规则

在计算机机中,软件技术和硬件技术有一个共同目标:在不改变程序执行结果的前提下尽可能提高并行度。编译器、处理器和JMM都同样遵从这一目标。

重排序对多线程的影响

int a=0;
boolean flag = false;
public void write(){
    a=1;             //1
    flag = true;     //2
}
public void reader(){
    if(flag){        //3 
        int i = a*a  //4
    }
}

如果有线程A和线程B,A执行write方法,B执行read方法。
因为操作1和操作2之间不存在依赖关系,编译器和处理器可以对这两个操作重排序;同样3和4也能重排序。
如果A优先修改了flag的值,那么B线程将读取a的值,此时变量a的值还没有被A线程写入,在这种情况下多线程的语义被重排序破坏了。

在程序中3和4操作存在控制依赖关系,当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测执行来克服控制相关性对并行度的影响。一上面代码为例,执行线程B的处理器可以提前读取并计算a*a ,然后把计算结果临时保存到一个名为重排序缓冲的硬件缓存中。当操作3的条件为真时,就把该计算结果写入变量i中。

在单线程程序中,对勋在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作作重排序,可能会改变程序的执行结果
比如:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值