高速缓存
cpu(CPU寄存器)<---> CPU高速缓存 <---> 主内存RAM
缓存一致性问题:
多个处理器的运算任务涉及统一块主内存时,可能导致内存不一致。为此需要个个处理器遵循一定协议,维护一致性。
cpu(CPU寄存器)<---> CPU高速缓存 <---> 缓存一致性协议 <---> 主内存RAM
java 内存模型 JMM(java memory model)
变量(Variables)与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字 段和构成数值对象的元素,但不包括局部变量与方法参数,
因为后者是线程私有 的。(如果局部变量是一个 reference 类型,它引用的对象在 Java 堆中可被各个线 程共享,但是 reference 本身在 Java 栈的局部变量表中,它是线程私有的)。
工作内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及 其他的硬件和编译器优化。
JVM 内存操作的并发问题
工作内存数据一致性 各个线程操作数据时会保存使用到的主内存中的 共享变量副本,当多个线程的运算任务都涉及同一个共享变量时,将导致 各自的的共享变量副本不一致,如果真的发生这种情况,数据同步回主内存以谁的副本数据为准? Java 内存模型主要通过一系列的数据同步协 议、规则来保证数据的一致性
指令重排序优化 Java 中重排序通常是编译器或运行时环境为了优化程 序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类: 编译期重排序和运行期重排序,分别对应编译时和运行时环境。 同样的, 指令重排序不是随意重排序,它需要满足以下两个条件
1 在单线程环境下不能改变程序运行的结果 即时编译器(和处理器)需要 保证程序能够遵守 as-if-serial 属性。通俗地说,就是在单线程情况下,要给 程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保 持一致。
2 存在数据依赖关系的不允许重排序
多线程环境下,如果线程处理逻辑之间存在依赖关系,有可能因为指令重排序导致运行结果与预期不同,可以声明用volatile关键字
- Java 内存间的交互操作
线程 1 和线程 2 都有主内存中共享变量 x 的副本,初始时,这 3 个内存中 x 的值 都为0。线程1中更新x的值为1之后同步到线程2主要涉及2个步骤:
- 1 线程 1 把线程工作内存中更新过的 x 的值刷新到主内存中
- 2 线程 2 到主内存中读取线程 1 之前已更新过的 x 变量
从整体上看,这 2 个步骤是线程 1 在向线程 2 发消息,这个通信过程必须经过主 内存。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线 程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过 主内存来完成,实现各个线程提供共享变量的可见性。
内存交互的基本操作
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工 作内存、如何从工作内存同步回主内存之类的实现细节,Java 内存模型中定义了 下面介绍 8 种操作来完成。
虚拟机实现时必须保证下面介绍的每种操作都是原子的,不可再分的(对于 double 和 long 型的变量来说,load、store、read、和 write 操作在某些平台上允 许有例外)
- lock (锁定) 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock (解锁) 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放
后的变量才可以被其他线程锁定。 - read (读取) 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内
存中,以便随后的 load 动作使用。 - load (载入) 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工
作内存的变量副本中。 - use (使用) 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,
每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。 - assign (赋值) 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内
存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 - store (存储) 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存
中,以便随后 write 操作使用。 - write (写入) 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放 入主内存的变量中。
- 线程内 从某个线程的角度看方法的执行,指令会按照一种叫“串行” (as-if-serial)的方式执行,此种方式已经应用于顺序编程语言。
- 线程间 这个线程“观察”到其他线程并发地执行非同步的代码时,由于指令 重排序优化,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步 方法,同步块(synchronized 关键字修饰)以及 volatile 字段的操作仍维持相对 有序。
- 程序次序规则 一个线程内,按照代码顺序,书写在前面的操作 happens-before 书 写在后面的操作。
- 锁定规则 一个 unLock 操作 happens-before 后面对同一个锁的 lock 操作。
- volatile 变量规则 对一个变量的写操作 happens-before 后面对这个变量的读操作。
- 传递规则 如果操作 A happens-before 操作 B,而操作 B 又 happens-before 操作 C,
则可以得出操作 A happens-before 操作 C。 - 线程启动规则 Thread 对象的 start()方法 happens-before 此线程的每个一个动作。
- 线程中断规则 对线程 interrupt()方法的调用 happens-before 被中断线程的代码检
测到中断事件的发生。 - 线程终结规则 线程中所有的操作都 happens-before 线程的终止检测,我们可以通
过 Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。 - 对象终结规则 一个对象的初始化完成 happens-before 他的 finalize()方法的开始
内存屏障
Java 中如何保证底层操作的有序性和可见性---可以通过内存屏障。
Store1;
Store2;
Load1;
StoreLoad; //内存屏障 Store3;
Load2;
Load3;
对于上面的一组 CPU 指令(Store 表示写入指令,Load 表示读取指令),StoreLoad 屏障之前的 Store 指令无法与 StoreLoad 屏障之后的 Load 指令进行交换位置,即 重排序。但是 StoreLoad 屏障之前和之后的指令是可以互换位置的,即 Store1 可 以和 Store2 互换,Load2 可以和 Load3 互换。
- LoadLoad 屏障: 对于这样的语句 Load1; LoadLoad; Load2,在 Load2 及后续读取操 作要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。
- StoreStore 屏障: 对于这样的语句 Store1; StoreStore; Store2,在 Store2 及后续写入 操作执行前,保证 Store1 的写入操作对其它处理器可见。
- LoadStore 屏障: 对于这样的语句 Load1; LoadStore; Store2,在 Store2 及后续写入操 作被执行前,保证 Load1 要读取的数据被读取完毕。
- StoreLoad 屏障: 对于这样的语句 Store1; StoreLoad; Load2,在 Load2 及后续所有读 取操作执行前,保证 Store1 的写入对所有处理器可见。它的开销是四种屏障中最大 的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个 万能屏障,兼具其它三种内存屏障的功能。
volatile 型变量的特殊规则
保证可见性 保证了不同线程对该变量操作的内存可见性。
线程写 volatile 变量的过程:
1 改变线程工作内存中 volatile 变量副本的值
2 将改变后的副本的值从工作内存刷新到主内存
线程读 volatile 变量的过程:
1 从主内存中读取 volatile 变量的最新值到线程的工作内存中
2 从工作内存中读取 volatile 变量的副本禁止进行指令重排序
当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全 部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能 把 volatile 变量后面的语句放到其前面执行。volatile 型变量实现原理
在每个 volatile 写操作的前面插入一个 StoreStore 屏障。 该屏障除了保证 了屏障之前的写操作和该屏障之后的写操作不能重排序,还会保证了 volatile 写操作之前,任何的读写操作都会先于 volatile 被提交。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。 该屏障除了使 volatile 写操作不会与之后的读操作重排序外,还会刷新处理器缓存,使 volatile 变量的写更新对其他线程可见。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。 该屏障除了使 volatile 读操作不会与之前的写操作发生重排序外,还会刷新处理器缓存, 使 volatile 变量读取的为最新值。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。 该屏障除了禁止 了 volatile 读操作与其之后的任何写操作进行重排序,还会刷新处理器缓 存,使其他线程 volatile 变量的写更新对 volatile 读操作的线程可见。