Java内存模型
Java内存模型
Java 内存模型的基础
并发编程模型的两个关键问题
- 线程之间如何通信 / 线程之间如何同步
- 共享内存(Java采用):
- 线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。
- 同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
- 消息传递:
- 线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。
- 消息的发送必须在消息的接收之前,因此同步是隐式进行的。
- 共享内存(Java采用):
Java 内存模型的抽象结构(JMM)
- JMM 定义了线程和主内存之间的抽象关系:
- 线程之间的共享变量存储在主内存(Main Memory)。
- 每个线程都有一个私有的本地内存(Local Memory):
- 本地内存中存储了该线程以读/写共享变量的副本。
- 本地内存是 JMM 的一个抽象概念,并不真实存在。
从源代码到指令序列的重排序
- 为了提高性能,编译器和处理器常常会对指令做重排序。
- 编译器优化的重排序(编译器重排序):
- 编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序(处理器重排序):
- 现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序(处理器重排序):
- 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
引用的对象有可能还没有完成初始化。
- 在线程执行到第 4 行,代码读取到
- 根源:
- 上面 3 行伪代码中的 2 和 3 之间,可能会被重排序。
- 解决方法:
- 把
instance
声明为volatile
型。 - 基于类初始化的解决方案:
- JVM 在类的初始化阶段(即在 Class 被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM 会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
- 把
Java 内存模型中的顺序一致性
同步原语
- volatile
- synchronized
- final
Java 内存模型的设计
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Comment