本文最后更新于 3 分钟前,文中所描述的信息可能已发生改变。
重点
- JMM 的工作方式
- 三大特征:原子性、可见性、有序性
- volatile 关键字
- volatile 与 synchronized 的区别
JMM 和 JVM
JMM 是 Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本,本地内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。而 JVM 则是描述的是 Java 虚拟机内部及各个结构间的关系。
- JVM 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。
- Java 内存模型(JMM) 和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
缓存不一致问题
解决 JMM 中的本地内存变量的缓存不一致问题有两种解决方案,分别是总线加锁和 MESI 缓存一致性协议
总线加锁
MESI protocol (缓存一致性协议)
MESI 缓存一致性协议是多个 CPU 从主内存读取同一个数据到各自的高速缓存中,当其中的某个 CPU 修改了缓存里的数据,该数据会马上同步回主内存,其它 CPU 通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。
在并发编程中,如果多个线程对同一个共享变量进行操作是,我们通常会在变量名称前加上关键在volatile ,因为它可以保证线程对变量的修改的可见性,保证可见性的基础是多个线程都会监听总线。即当一个线程修改了共享变量后,该变量会立马同步到主内存,其余线程监听到数据变化后会使得自己缓存的原数据失效,并触发 read 操作读取新修改的变量的值。进而保证了多个线程的数据一致性。事实上, volatile的工作原理就是依赖于 MESI 缓存一致性协议实现的。
happens-before 原则
happens-before 原则表达的意义其实并不是一个操作发生在另外一个操作的前面,虽然这从程序员的角度上来说也并无大碍。更准确地来说,它更想表达的意义是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。
内存交互操作
- lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
- read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的 load 操作使用。
- load(加载),作用于工作内存的变量,把 read 操作主存的变量放入到工作内存的变量副本中。
- use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
- store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的 write 使用。
- write(写入):作用于主内存中的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
内存交互操作规则
不允许 read、load、store、write 操作之一单独出现,也就是 read 操作后必须 load,store 操作后必须 write。
不允许线程丢弃他最近的 assign 操作,即工作内存中的变量数据改变了之后,必须告知主存。
不允许线程将没有 assign 的数据从工作内存同步到主内存。
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施 use、store 操作之前,必须经过 load 和 assign 操作。
一个变量同一时间只能有一个线程对其进行 lock 操作。多次 lock 之后,必须执行相同次数 unlock 才可以解锁。
如果对一个变量进行 lock 操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新 load 或 assign 操作初始化变量的值。
如果一个变量没有被 lock,就不能对其进行 unlock 操作。也不能 unlock 一个被其他线程锁住的变量。
一个线程对一个变量进行 unlock 操作之前,必须先把此变量同步回主内存。
Program Order Rule: Each action in a thread happens-before every action in that thread that comes later in the program order.
Monitor Lock Rule: An unlock on a monitor lock happens-before every subsequent lock on that same monitor lock.
Volatile Variable Rule: A write to a volatile field happens-before every subsequent read of that same field.
Thread Start Rule: A call to Thread.start on a thread happens-before any action in the started thread.
Thread Termination Rule: Any action in a thread happens-before any other thread detects that thread has terminated, either by successfully return from Thread.join or by Thread.isAlive returning false.
Transitivity: If A happens-before B, and B happens-before C, then A happens-before C.
volatile
volatile 是 Java 提供的一种轻量级的同步机制,它具有两个特性:可见性和有序性。
特性
- 可见性:volatile 保证了线程间变量是可见的,即当一个线程修改了共享变量后,该变量会立马同步到主内存,其余线程监听到数据变化后会使得自己缓存的原数据失效,并触发 read 操作读取新修改的变量的值。
- 有序性:volatile 保证了线程对变量的修改是有序的,即禁止指令重排序。
- 当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。
- 在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。
应用场景
- volatile 不能保证原子性,即不能保证多个线程同时修改一个变量时的线程安全性。
- volatile 不能代替锁,它只能保证可见性和有序性,不能保证原子性。
- volatile 适用于一个线程写,多个线程读的场景。
总结
- Java 是最早尝试提供内存模型的语言,其主要目的是为了简化多线程编程,增强程序可移植性的。
- CPU 可以通过制定缓存一致协议(比如 MESI 协议)来解决内存缓存不一致性问题。
- 为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致,所以在多线程下,指令重排序可能会导致一些问题。
- 你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
- JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性。