Skip to content

Latest commit

 

History

History
184 lines (110 loc) · 6.9 KB

File metadata and controls

184 lines (110 loc) · 6.9 KB

基础

通信

  • 线程之间以何种机制来交换信息
  • 主要方式
    • 共享内存(隐式通信)
    • 消息传递(显式通信)

同步

  • 程序中用于控制不同线程操作发生的相对顺序
  • 同步方式
    • 共享内存(显式同步)
    • 消息传递(隐式同步)

JMM的抽象结构

共享变量、局部变量

image.png

指令重排

  • 编译器优化的重排序

  • 指令级别的并行重排序

    两个指令之间不存在数据依赖即可进行指令的重排序

  • 内存系统的重排序

    image.png
  • as-if-serial

    ​ 不管怎么重排序,单线程程序的执行结果不可被改变。编译器、runtime和处理器必须遵守 as-if-serial

  • happens-before

    • 提供跨线程的内存可见性保证

    • JMM 设计

      • 为程序员提供足够强的内存可见性保证
      • 对编译器和处理器的限制要尽可能地放松
      • 必须禁止会改变程序运行结果的指令重排序
    • 如果一个操作的执行结果需要对另一个操作可见,这两个操作之间必须存在 happens-before。

      • 程序顺序规则:一个线程中的每个操作,happens-before 在该线程中的任意后续操作。
      • 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
      • volatile 变量规则:对一个 volatile 域的写,happens-before于任意后续对这个 volatile 域的读。
      • 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
      • start() 规则:如果线程 A 执行操作 Thread.start()(启动线程 B), 那么线程 A 中的任意操作 happens-before 线程 B 中的任意操作。
      • join() 规则:如果线程 A 执行操作 Thread.join() 并且成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 Thread.join() 操作成功返回。
      • 线程中断规则:对线程 interrupt 方法的调用 happens-before 于被中断线程的代码检测到中断事件的发生。
      • 对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before 它的 finalize() 方法
      image.png

Synchronized

  • 修饰代码块
  • 修饰方法

锁的内存语义

  • 锁的释放:获取建立的 happens-before 关系

    ​ 锁除了使得临界区互斥之外,还可以让释放锁的线程向获取同一个锁的线程发送消息,建立 happens-before 关系。

    public class MonitorExample {
    	int a = 0;
    
    	public synchronized void writer() { // 1
    		a++; // 2
    	} // 3
    
    	public synchronized void reader() { // 4
    		int i = a; // 5;
    	} // 6;
    }

    ​ 假设线程 A 首先执行 writer() 方法,随后线程 B 执行 reader() 方法。按照 happens-before 规则,这个过程分为以下几步:

    • 根据程序次序规则,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6

    • 按照监视器锁规则,3 happens-before 4

    • 按照传递性规则,2 happens-before 5

      image.png
    • 在线程A释放了锁之后,随后线程B获取同一个锁。因此线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对B线程可见

  • 锁释放的内存语义

    当线程释放锁时,JMM 会把该线程对应的(所有)本地内存中的共享变量刷新到主内存中

    以面的代码为例,A 线程释放锁之后,共享数据示意图如下所示:

    image.png
  • 锁获取的语义

    ​ 当线程获取锁时,JMM 会把该线程对应的(所有)本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

    依旧一上文代码为例,线程 B 在获取到锁之后,共享数据状态如下所示:

    image.png

volatile 的内存语义

对单一变量的写对所有线程都是实时可见的。

  • volatile 写

    当写一个volatile变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

    image.png
  • volatile 读

    ​ 当读一个volatile变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

    image.png

内存屏障

内存屏障是一种barrier指令类型,它导致CPU或编译器对barrier指令前后发出的内存操作执行顺序约束。也就是说,在barrier之前的内存操作保证在barrier之后的内存操作之前执行。

2021-07-03 17-19-43 的屏幕截图.png

  • 在每个volatile写操作的前面插入一个 StoreStore 屏障

  • 在每个volatile写操作的后面插入一个 StoreLoad 屏障

    2021-07-03 19-06-57 的屏幕截图.png

  • 在每个volatile读操作的后面插入一个 LoadLoad 屏障

  • 在每个volatile读操作的后面插入一个 LoadStore 屏障

    2021-07-03 19-07-05 的屏幕截图.png

final 内存语义

​ 写一个 final 域不能重排序到构造函数外,读 final 域不能重排序到读该引用对象之前。

  • 在构造函数内对一个 final 域的写入,与之后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
  • 初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不可重排序。

​ 写 final 域的重排序规则

  • 对于基本数据类型
    • JMM 禁止编译器把 final 域的写重排序到构造函数之外
    • 编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障
  • 对于引用类型
    • 在构造函数内对一个 final 引用对象的成员域的写入,与随后在构造函数外把该被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。(这一规则加强了 final,使得 String 不再可能会变)

读 final 域的重排序规则

  • 对于基本数据类型
    • 在一个线程中,初次读该对象与初次读该对象包含的 final 域, JMM 禁止重排序这两个操作