Java内存模型

Java 内存模型的基础

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

  • 线程之间如何通信 / 线程之间如何同步
    • 共享内存(Java采用)
      • 线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信
      • 同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
    • 消息传递
      • 线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信
      • 消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java 内存模型的抽象结构(JMM)

  • JMM 定义了线程和主内存之间的抽象关系
    • 线程之间的共享变量存储在主内存(Main Memory)
    • 每个线程都有一个私有的本地内存(Local Memory)
      • 本地内存中存储了该线程以读/写共享变量的副本。
      • 本地内存是 JMM 的一个抽象概念,并不真实存在。

Java 内存模型

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

  • 为了提高性能,编译器和处理器常常会对指令做重排序。
    • 编译器优化的重排序(编译器重排序)
      • 编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
    • 指令级并行的重排序(处理器重排序)
      • 现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
    • 内存系统的重排序(处理器重排序)
      • JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers)指令,通过内存屏障指令来禁止特定类型的处理器重排序

happens-before 规则

  • happens-before 规则阐述操作之间的内存可见性
    • 在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。
    • happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。

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 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的 ThreadB.start()操作 happens-before 于线程 B 中的任意操作。
  • join()规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。

volatile 的特性

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

双重检查锁定与延迟初始化

双重检查锁定

  • 问题
    • 在线程执行到第 4 行,代码读取到 instance 不为 null 时,instance 引用的对象有可能还没有完成初始化。
  • 根源
    • 重排序问题
    • 上面 3 行伪代码中的 2 和 3 之间,可能会被重排序。
  • 解决方法
    • instance 声明为 volatile 型。
    • 基于类初始化的解决方案:
      • JVM 在类的初始化阶段(即在 Class 被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM 会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
      • 类初始化锁

Java 内存模型中的顺序一致性

同步原语

  • volatile
  • synchronized
  • final

Java 内存模型的设计