Java 内存模型笔记

Published by 煎鱼 on

之前为了面试,整理过一堆笔记

JMM 定义了 Java 虚拟机 (JVM) 在计算机内存 (RAM) 中的工作方式。JVM 是整个计算机虚拟模型,所以 JMM 是隶属于 JVM 的。

并发编程

两个关键问题:线程之间的通信和同步

线程之间的通信

在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 – 读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信

在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在 java 中典型的消息传递方式就是 wait() 和 notify()

线程之间的同步

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。

在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java 的并发采用的是共享内存模型

Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

Java 内存模型

JMM 决定一个线程对共享变量的写入何时对另一个线程可见。JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读 / 写共享变量的副本。

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

通信过程必须要经过主内存。

当对象和变量存储到计算机的各个内存区域时,最主要的两个问题是:

  1. 共享对象对各个线程的可见性
  2. 共享对象的竞争现象

可见性

如果没有合理的使用 volatile 和 synchronization 关键字,一个线程对共享对象的更新有可能导致其它线程不可见。

volatile 关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。

竞争现象

如果多个线程共享一个对象,如果它们同时修改这个共享对象,这就产生了竞争现象。

可以使用 java synchronized 代码块。synchronized 代码块可以保证同一个时刻只能有一个线程进入代码竞争区,synchronized 代码块也能保证代码块中所有变量都将会从主存中读,当线程退出代码块时,对所有变量的更新将会 flush 到主存,不管这些变量是不是 volatile 类型的。

支撑 Java 内存模型的基础原理

指令重排序

  • 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

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

内存屏障(Memory Barrier )

  • 保证特定操作的执行顺序。
  • 影响某些数据(或则是某条指令的执行结果)的内存可见性。

插入一条 Memory Barrier 会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。

Memory Barrier 所做的另外一件事是强制刷出各种 CPU cache,如一个Write-Barrier(写入屏障)将刷出所有在 Barrier 之前写入 cache 的数据,因此,任何 CPU 上的线程都能读取到这些数据的最新版本。(volatile 是基于 Memory Barrier 实现的。)volatile修饰时,JMM 会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令

happens-before

在 JMM 中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在 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。

通俗解释

如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。

如果线程1写入了volatile变量v(这里和后续的“变量”都指的是对象的字段、类字段和数组元素),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。

1:a = 1;
2:list.set(1,"t");
// p1 < p2

线程t1写入的所有变量(所有action都与那个join有hb关系,当然也包括线程t1终止前的最后一个action了,最后一个action及之前的所有写入操作,所以是所有变量),在任意其它线程t2调用t1.join()成功返回后,都对t2可见。

线程中上一个动作及之前的所有写操作在该线程执行下一个动作时对该线程可见(也就是说,同一个线程中前面的所有写操作对后面的操作可见)

先这样吧

若有错误之处请指出,更多地关注煎鱼

Categories: Java

发表评论

电子邮件地址不会被公开。 必填项已用*标注