内存模型

并发编程的问题

关键问题

  1. 线程之间如何通信
  2. 线程之间如何同步

    通信机制

  3. 共享内存

  4. 消息传递

    注:Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行.

JMM的抽象结构

JMM概述

JMM决定一个线程对共享变量的写入何时对另一个线程可见。
线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的个抽象概念,并不真实存在。

抽象示意图

image.png
线程A与线程B之间通信的步骤:

  1. 线程A把本地内存A中更新过的共享变量刷新到主内存中去
  2. 线程B到主内存中去读取线程A之前已更新过的共享变量

详细的通信步骤:
image.png
本地内存A和本地内存B由主内存中共享变量x的副本。假设初始时,这3个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,为开发人员提供内存可见性保证。

指令的重排序

指令的重排序分类

  1. 编译器优化的重排序
  2. 指令级并行的重排序
  3. 内存系统的重排序

    编译器优化的重排序

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

    指令级并行的重排序

    现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

    内存系统的重排序

    由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

    并发编程模型分类

    d02cc662658e86518aa8f3a6691b3a8.jpg

    happens-before

    在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。

    与程序员相关的happens-before规则

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

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

与JMM的关系图

image.png

重排序

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

数据依赖性

概念

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

分类

  • 写后读:写一个变量之后,再读这个变量;
  • 写后写:写一个变量之后,再写这个变量;
  • 读后写:都一个变量之后,再写这个变量。

    注:上述三种分类,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。

as-if-serial语义

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、运行时、处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。