Java并发 Java内存模型 happens-before原则

2019-08-03  本文已影响0人  baiiu

1. 两个关键问题

并发编程中,需要处理两个关键问题:线程之间如何通信 和 线程之间如何同步。
通信是指线程之间怎样交换信息。同步是指怎样控制不同线程间操作发生的相对顺序。

在命令式编程中,线程之间的通信机制有两种:共享内存 和 消息传递。

在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信;并且它的同步也是显示进行的,必须显示指定某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,线程之间没有公共状态,必须通过发送消息来显示进行通信;但是他的同步是隐式进行的,因为消息的发送必须在消息的接收之前。

Java的并发采用的是共享内存模型。接下来我们去理解Java是怎样解决这两个的问题的。即Java的线程之间是如何通信、同步的。

2. Java内存模型

虚拟机运行时数据区域

Java运行时数据区域我们知道,方法区和堆是由所有线程共享的数据区域。 虚拟机栈、本地方法栈和程序计数器是线程私有的内存。

Java线程之间的通信由Java内存模型(JMM)控制。JMM决定了一个线程对共享变量的写入何时对另一个线程可见。
JMM的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。

本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。 对应虚拟机栈中的部分区域。

Java内存模型的抽象结构示意图
线程、主内存、工作内存三者关系

线程A与线程B怎样通信呢:

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

JMM通过控制主内存和每个线程的本地内存之间的交互,来保证内存的可见性。

3. 重排序

在执行程序时,编译器和处理器会对指令进行重排序以提高性能。

  1. 编译器优化的重排序。
    编译器在不改变单线程程序语义的前提下可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。
    指令级并行处理技术可以将多条指令重叠执行,如果不存在数据依赖,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。
    处理器可能因为使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行。
从源代码到最终执行的重排序过程

这些重排序可能会导致多线程程序出现内存可见性问题:
1. 编译器重排序(第1种类型)规则可以禁止特定类型的编译型重排序。
2. 处理器重排序(第2、3种类型),JMM的处理器重排序规则会要求编译器在生成指令序列时插入指定的内存屏障指令(Memory Barriers)来禁止指令重排序。

4. 内存交互操作和内存屏障

Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。

内存模型的8个操作

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

这8种内存访问操作很繁琐,后文会使用一个等效判断原则,即先行发生(happens-before)原则来确定一个内存访问在并发环境下是否安全。
根据happens-before原则在多线程环境下,需要插入内存屏障来确保同步的安全性。

StoreLoad Barriers是一个全能型屏障,它同时具有其他3个屏障的效果。

5. happens-before原则

参考:
Java并发
深入理解


上一篇 下一篇

猜你喜欢

热点阅读