《JAVA并发编程的艺术》读书笔记③

JAVA内存模型的基础

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

  • 在共享内存的并发模型中,线程之间共享程序的公共状态,通过读写内存中的公共状态来进行隐式通信。
  • 在消息传递的并发模型中,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。

JAVA的并发采用的是共享内存模型

JAVA内存模型的抽象结构

  • 在JAVA中,所有实例域、静态域和数组元素都在堆内存中,堆内存在线程之间共享。
  • 局部变量、方法定义参数和异常处理函数,在栈内存里面,不会在线程之间共享。

image.png

线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存存储了该内存以读写共享变量的副本。
A与B之间的通信,需要通过以下2个步骤

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

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

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

  • 编译器优化的重排序:编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序。
  • 内存系统的重排序:由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是乱序执行。

image.png

对于指令重排,JAVA内存模型的处理器重排序规则可以要求JAVA编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。

内存屏障:指的是重排序时不能把后面的指令重排序到内存屏障之前的位置。

展示一个由于内存操作重排带来问题的例子(经典)

在展示之前,首先补充一点知识:现代处理器使用写缓存区临时保存向内存下入的数据,以保证指令流水线持续运行,避免处理器停顿下来等待向内存写入数据而产生延迟,同时,以批处理的方式刷新缓存区,以及合并写缓存区对统一内存地址的多次写,减少堆内存总线的占用。

这个特性会对内存操作的顺序产生影响:处理器堆内存的读写操作的执行顺序,不一定与内存实际发生的读写操作顺序一致(因为内存操作重排)

image.png

如果AB同时执行,可能会得到读取到脏数据,原因如下:

image.png

此处由于指令重排,导致A1->A2的顺序变成了A2->A1,所以读取了脏数据。

一般情况下,处理器都不允许对存在数据依赖的操作做重排序。

happens-before

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

A happens-before B, JMM 并不要求A一定要在B之前执行, JMM仅仅要求前一个操作的结果对后一个操作课件,且前一个操作按顺序排在第二个操作之前.

本文标题:《JAVA并发编程的艺术》读书笔记③

文章作者:Enda Lin

发布时间:2019年06月02日 - 19:20

最后更新:2019年09月12日 - 15:48

原始链接:https://wt-git-repository.github.io/2019/06/02/《JAVA并发编程的艺术》读书笔记③/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。