美文网首页
java线程之内存模型

java线程之内存模型

作者: dimdark | 来源:发表于2018-03-25 12:12 被阅读0次

参考书籍: <<java并发编程的艺术>>
这篇文章是自己阅读该书籍时的读书笔记

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

在并发模型中, 常常要处理两个关键的问题:

  • 线程之间如何通信
  • 线程之间如何同步
    通信 : 线程之间以何种机制来交换信息;
    同步: 线程中用于控制不同线程之间操作发生相对顺序的机制

常见的通信机制有两种: 共享内存消息传递

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

共享内存并发模型中, 同步显示进行的;
消息传递并发模型中, 同步隐式进行的;
java的并发采用的是共享内存模型(因此需要显示进行同步)

2. java内存模型的抽象结构
java内存模型的抽象结构示意图
3. 数据依赖性

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

数据依赖类型

注意:

编译器和处理器在重排序时, 会遵守数据依赖性, 编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序;

这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作, 不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑;

4. 重排序

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

重排序的类型

注意: 内存系统的重排序可能导致处理器对内存的写/读操作的执行顺序不一定与内存实际发生的读/写操作顺序一致

对于编译器的重排序, JMM的编译器重排序规则会禁止特定类型的编译器重排序;
对于处理器的重排序, JMM的处理器重排序规则会要求java编译器在生成指令序列时, 插入特定类型的内存屏障(Memory Barriers)指令, 通过内存屏障指令来禁止特定类型的处理器重排序

内存屏障的类型
JMM这样做的目的是: 为程序员提供一致的内存可见性的保证
5. 顺序一致性内存模型(理论模型 参考模型)

顺序一致性内存模型的两大特性:
- 一个线程中的所有操作必须按照程序的顺序来执行;
- (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序, 在顺序一致性内存模型中, 每个操作都必须原子执行且立刻对所有线程可见;

顺序一致性内存模型为程序员提供的视图

注意: 正确同步的多线程程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同(即可以利用程序在顺序一致性内存模型的执行结果来判断自己编写的多线程程序是否符合期望)

6. volatile 的内存语义
  • volatile 的特性
    可见性: 对一个volatile变量的读, 总是能看到(任意线程)对这个volatile变量最后的写入;
    原子性: 对任意单个volatile变量的读/写具有原子性, 但类似于volatile++这种复合操作不具有原子性;

  • volatile 写-读的内存语义
    当写一个 volatile 变量时, JMM会把该线程对应的本地内存中所有的共享变量的值刷新到主内存;
    当读一个 volatile 变量时, JMM会把该线程对应的本地内存置为无效, 线程接下来从主内存中读取共享变量;

  • volatile 内存语义的实现

    JMM针对编译器指定的volatile重排序规则

    编译器的volatile重排序规则
    1. 当第二个操作是 volatile 时, 不管第一个操作是什么, 都不能重排序(这个规则确保 volatile 之前的操作不会被编译器重排序到 volatile写 之后)
    2. 当第一个操作是 volatile 时, 不管第二个操作是什么, 都不能重排序(这个规则确保 volatile 之后的操作不会被编译器重排序到 volatile 之前;
    3. 当第一个操作是 volatile, 第二个操作是 volatile 时, 不能重排序;

    JMM针对处理器指定的volatile重排序规则
    为了实现 volatile 的内存语义, JMM采取保守策略, 编译器在生成字节码时, 会在指令序列中插入内存屏障来禁止特定类型的处理器重排序;

    处理器的volatile重排序规则
    volatile写的指令序列示意图
    volatile读的指令序列示意图
7. 锁的内存语义
  • 锁的释放和获取的内存语义
    当线程释放锁时, JMM会把该线程对应的本地内存中的共享变量刷新到主内存中;
    当线程获取锁时, JMM会把线程对应的本地内存置为无效;

注意: 锁释放与volatile写有相同的内存语义; 锁获取与volatile读有相同的内存语义;

  • 锁释放-获取的内存语义的实现
    1. 利用 volatile 变量的写-读所具有的内存语义;
    2. 利用 CAS 所附带的 volatile 读 和 volatile 写的内存语义;
8. final 域的内存语义
  • final 域的重排序规则

    1. 在构造函数内对一个 final域 的写入, 与随后把这个被构造对象的引用赋值给一个引用变量, 这两个操作之间不能重排序;(
      final
      )
    2. 初次读一个包含 final 域 的对象的引用, 与随后初次读这个final域, 这两个操作之间不能重排序; (final)
  • final 域的重排序规则
    final 域的重排序规则禁止把 final 域的写重排序到构造函数之外, 实现该规则需要:

    1. JMM禁止编译器把 final 域的写重排序到构造函数之外;
    2. 编译器会在 final 域的写之后, 构造函数return之前, 插入一个 StoreStore屏障(这个屏障禁止处理器把 final 域的写重排序到构造函数之外)

final 域的重排序规则可以保证在对象引用为任意线程可见之前, 对象的 final 域已经被正确初始化过了

  • final 域的重排序规则
    final 域的重排序规则禁止处理器重排序初次读一个包含 final 域对象的引用和初次读这个 final, 实现该规则需要:
    编译器在读 final 域操作的前面插入一个 LoadLoad 屏障

final 域的重排序规则可以保证在读一个对象的 final 域之前, 一定会先读包含这个 final 域的对象的引用

  • 写引用类型的 final 域的额外重排序规则
    对于引用类型, 写 final 域的重排序规则对编译器和处理器增加了如下约束:
    在构造函数内对一个 final 引用的对象的成员域的写入, 与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量, 这两个操作之间不能重排序

注意: final 的内存语义向程序员保证了只要对象是正确构造的(被构造对象的引用在构造函数中没有"逸出")则不需要使用同步就可以保证任意线程都能看到这个 final 域在构造函数中被初始化之后的值

9. happens-before 规则(JMM对程序员的承诺)

1). 程序顺序规则
一个线程中的每一个操作, happens-before 于该线程中的任意后序操作;

2). 监视器锁规则
对一个锁的解锁, happens-before 于随后对这个锁的加锁;

3). volatile变量规则
对一个volatile域的写, happens-before于任意后续对这个volatile域的读;

4). 传递性
如果A happens-before B, 且B happens-before C, 那么 A happens-before C;

5). start()规则
如果线程A执行操作ThreadB.start()(启动线程B), 那么A线程的ThreadB.start()操作 happens-before 于线程B中的任意操作;

6). join()规则
如果线程A执行操作ThreadB.join()并成功返回, 那么线程B中的任意操作 happens-before 于线程A从 ThreadB.join()操作成功返回(的后序操作);

7). 线程终结规则
线程中所有的操作都先行发生于线程的终止检测;

8). 对象终结规则
一个对象的初始化完成先行于它的finalize()方法的开始;

相关文章

网友评论

      本文标题:java线程之内存模型

      本文链接:https://www.haomeiwen.com/subject/vwuccftx.html