Java并发编程的艺术——读书笔记

Java内存模型的基础

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

1.线程间如何通信?
通信是指线程之间以何种机制来交换信息。
方法一:共享内存;线程间共享程序的公共状态,通过读写内存中的公共状态进行隐式通信。Java语言就是采用的共享内存模型。
方法二:消息传递。线程之间没有公共状态,线程之间必须通过发送消息来显示进行通信。

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

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

Java内存模型的抽象结构

由上可知,Java采用的是共享内存模型,所以通信是由Java内存模型(JMM)控制,通过读写内存中的公共状态进行隐式通信,JMM决定一个线程对共享变量的写入何时对另外一个线程可见。

注意:共享变量通常是指存放在堆内存中的实例域、静态域、数组元素。而局部变量、方法定义参数、异常处理参数是不会在线程之间共享的,也就不存在相应的线程安全问题。

JMM定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主内存中,每个线程都有各自的一个私有的本地内存,本地内存中存储了该线程用到的读写共享变量的副本。

通信过程:线程A修改本地内存中的共享变量副本,同步到主内存中,线程B读取主内存中的共享变量到自己的本地内存,从而实现读写内存中的公共状态进行隐式通信

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

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

  1. 编译器优化的重排序;编译器在不改变单线程程序语义的前提下,重新安排语句顺序
  2. 指令级并行的重排序;
  3. 内存系统的重排序。

上述的1属于编译器重排序,23属于处理器重排序。
在多线程环境,重排序可能会导致内存可见性问题,也就是出现一些错误。
对于编译器重排序,JMM的编译器重排序规则会禁止特定类型的编译器重排序。
对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过该内存屏障来禁止特定类型的处理器重排序。

注意:java是一次编译,到处运行的语言,所以对于JMM这一语言级别的内存模型,它确保在不同的编译器和不同的处理器平台上,通过禁止特定类型的编译器重排和处理器重排序,为程序猿提供一致的内存可见性保证。

并发编程模型的分类

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

也是由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作进行重排序。(都不允许对存在数据依赖的操作做重排序。)

为了保证内存的可见性,也就是不出现上述的重排序带来的不良影响,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。
JMM把内存屏障指令分为4类,LoadLoad Barriers;StoreStore Barriers;LoadStore Barriers;StoreLoad Barriers。其中最后一个,StoreLoad Barriers 是一个全能的屏障,它同时支持其他三个屏障的效果,也是现在大多数处理器都支持的。
但是执行该屏障开销昂贵,因为当前处理器通常要把写缓冲区的数据全部刷新到内存中。

happens-before简介

文章目录
  1. 1. Java内存模型的基础
    1. 1.1. 并发编程模型中的两个关键问题
    2. 1.2. Java内存模型的抽象结构
    3. 1.3. 从源代码到指令序列的重排序
    4. 1.4. 并发编程模型的分类
    5. 1.5. happens-before简介
|