写点什么

【并发编程】- 内存模型篇

用户头像
双木之林
关注
发布于: 2021 年 01 月 19 日
【并发编程】- 内存模型篇

并发编程模型

1.两个关键问题

1)线程之间如何通信

共享内存

程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信

消息传递

程之间没有公共状态,线程之间必须通过发送消息来显式进行通信

2)线程之间如何同步

线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信


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

  • 2.抽象结构

  • 1)本地内存

  • 每个线程都有一个私有的本地内存(LocalMemory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化



  • 2)主内存

  • 线程之间的共享变量存储在主内存


附图:



注:所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享(本章用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local Variables),方法定义参数(Java 语法规范称之为 Formal Method Parameters)和异常处理器参数(Exception HandlerParameters)不会在线程之间共享,它们不会有内存可⻅性问题,也不受内存模型的影响。


  • 3.重排序



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



  • 1) 3 种类型

  • 编译器优化的重排序(处理器重排序)

  • 编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序

  • 指令级并行的重排序(处理器重排序)



现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序内存系统的重排序由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

  • 2)数据依赖性



  • 说明:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。



  • 存在三种类型(只要重排两个操作执行顺序,结果便会被改变)写后读:写一个变量之后,再读这个变量写后写:写一个变量之后,在写这个变量读后写:读一个变量之后,再写这个变量


  • 3)as-if-serial 语义


说明:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime 和处理器都必须遵守 as-if-serial 语义。

顺序规则:A happens-before B,B happens-before C,happens-before C

那么实际执行是B可以排在A前,JMM 允许这种排序。


  • 4)对多线程的影响


  • 对于存证控制依赖的操作重排序,可能会改变程序的执行结果。


  • 5)DCL 问题(double check lock)


    public class DoubleCheckedLocking { // 1        private static Instance instance; // 2
public static Instance getInstance() { // 3 if (instance == null) { // 4:第一次检查 synchronized (DoubleCheckedLocking.class) { // 5:加锁 if (instance == null) // 6:第二次检查 instance = new Instance(); // 7:问题的根源出在这里 } // 8 } // 9 return instance; // 10 } // 11 }
复制代码

那么问题就出现在线程执行到第 4 行,代码读取到 instance 不为 null 时,instance 引用的对象有可能还没有完成初始化

  • 根源:


    memory = allocate();  //1:分配对象的内存空间    ctorInstance(memory);  //2:初始化对象    instance = memory;   //3:设置instance指向刚分配的内存地址
复制代码
  • JMM 允许上述命令的执行顺序调整为


    memory = allocate();  //1:分配对象的内存空间    instance = memory;   //3:设置instance指向刚分配的内存地址                          //注意,此时对象还没有被初始化!    ctorInstance(memory); //2:初始化对象
复制代码

问题:为什么要调整这个顺序呢?

原因:这个重排序在没有改变单线程程序执行结果的前提下,可以提高程序的执行性能。



  • 解决方案

  • 第一种基于volatile方案


    public class SafeDoubleCheckedLocking {        private volatile static Instance instance;
public static Instance getInstance() { if (instance == null) { synchronized (SafeDoubleCheckedLocking.class) { if (instance == null) instance = new Instance(); // instance为volatile,现在没问题了 } } return instance; } }
复制代码
  • 第二种基于类初始化方案


    public class InstanceFactory {        private static class InstanceHolder {            public static Instance instance = new Instance();        }
public static Instance getInstance() { return InstanceHolder.instance;  // 这里将导致InstanceHolder类被初始化,存在初始化锁,拿不到的线程会一直等待 } }
复制代码



  • 4.happens-before


happens-before是JMM最核心的概念

① 关系的定义

  • 如果⼀个操作 happens-before 另⼀个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。[JMM对程序员的承诺]

  • 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM 允许这种重排序)[JMM对编译器和处理器重排序的约束原则]


② 规则

  • 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。

  • 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。

  • volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

  • 传递性:A happens-before B,且 B happens-before C,那么 A happens-before C。

  • start()规则:如果线程 A 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的 ThreadB.start()操作 happens-before 于线程 B 中的任意操作。

  • join()规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。


注意:两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第一个操作之前(the first is visible to and ordered before the second)。

appens-before 与 JMM 的关系


话外语:

  • as-if-serial 语义保证单线程内程序的执行结果不被改变,happens-before 关系保证正确同步的多线程程序的执行结果不被改变

  • as-if-serial 语义和 happens-before 这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度


用户头像

双木之林

关注

还未添加个人签名 2020.05.09 加入

互联网金融开发工程师

评论

发布
暂无评论
【并发编程】- 内存模型篇