📦 本文以及示例源码已归档在 javacorehtml
Java 内存模型(Java Memory Model),简称 JMM。java
JVM 中试图定义一种 JMM 来屏蔽各类硬件和操做系统的内存访问差别,以实现让 Java 程序在各类平台下都能达到一致的内存访问效果。git
物理机遇到的并发问题与虚拟机中的状况有很多类似之处,物理机对并发的处理方案对于虚拟机的实现也有至关大的参考意义。github
物理内存的第一个问题是:硬件处理效率。编程
高速缓存解决了 硬件效率问题,可是引入了一个新的问题:缓存一致性(Cache Coherence)。缓存
在多处理器系统中,每一个处理器都有本身的高速缓存,而它们又共享同一主内存。当多个处理器的运算任务都涉及同一块主内存区域时,将可能致使各自的缓存数据不一致。安全
为了解决缓存一致性问题,须要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操做。markdown
除了高速缓存之外,为了使得处理器内部的运算单元尽可能被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化。处理器会在计算以后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但不保证程序中各个语句计算的前后顺序与输入代码中的顺序一致。多线程
乱序执行技术是处理器为提升运算速度而作出违背代码原有顺序的优化。架构
内存模型
这个概念。咱们能够理解为:在特定的操做协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不一样架构的物理计算机能够有不同的内存模型,JVM 也有本身的内存模型。
JVM 中试图定义一种 Java 内存模型(Java Memory Model, JMM)来屏蔽各类硬件和操做系统的内存访问差别,以实现让 Java 程序 在各类平台下都能达到一致的内存访问效果。
JMM 的主要目标是 定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数值对象的元素,但不包括局部变量与方法参数,由于后者是线程私有的,不会被共享,天然就不会存在竞争问题。为了得到较好的执行效能,JMM 并无限制执行引擎使用处理器的特定寄存器或缓存来和主存进行交互,也没有限制即便编译器进行调整代码执行顺序这类优化措施。
JMM 规定了全部的变量都存储在主内存(Main Memory)中。
每条线程还有本身的工做内存(Working Memory),工做内存中保留了该线程使用到的变量的主内存的副本。工做内存是 JMM 的一个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其余的硬件和编译器优化。
线程对变量的全部操做都必须在工做内存中进行,而不能直接读写主内存中的变量。不一样的线程间也没法直接访问对方工做内存中的变量,线程间变量值的传递均须要经过主内存来完成。
说明:
这里说的主内存、工做内存与 Java 内存区域中的堆、栈、方法区等不是同一个层次的内存划分。
相似于物理内存模型面临的问题,JMM 存在如下两个问题:
as-if-serial
属性。通俗地说,就是在单线程状况下,要给程序一个顺序执行的假象。即通过重排序的执行结果要与顺序执行的结果保持一致。 JMM 定义了 8 个操做来完成主内存和工做内存之间的交互操做。JVM 实现时必须保证下面介绍的每种操做都是 原子的(对于 double 和 long 型的变量来讲,load、store、read、和 write 操做在某些平台上容许有例外 )。
lock
(锁定) - 做用于主内存的变量,它把一个变量标识为一条线程独占的状态。 unlock
(解锁) - 做用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定。 read
(读取) - 做用于主内存的变量,它把一个变量的值从主内存传输到线程的工做内存中,以便随后的 load
动做使用。 write
(写入) - 做用于主内存的变量,它把 store 操做从工做内存中获得的变量的值放入主内存的变量中。 load
(载入) - 做用于工做内存的变量,它把 read 操做从主内存中获得的变量值放入工做内存的变量副本中。 use
(使用) - 做用于工做内存的变量,它把工做内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个须要使用到变量的值得字节码指令时就会执行这个操做。 assign
(赋值) - 做用于工做内存的变量,它把一个从执行引擎接收到的值赋给工做内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做。 store
(存储) - 做用于工做内存的变量,它把工做内存中一个变量的值传送到主内存中,以便随后 write
操做使用。 若是要把一个变量从主内存中复制到工做内存,就须要按序执行 read
和 load
操做;若是把变量从工做内存中同步回主内存中,就须要按序执行 store
和 write
操做。但 Java 内存模型只要求上述操做必须按顺序执行,而没有保证必须是连续执行。
JMM 还规定了上述 8 种基本操做,须要知足如下规则:
上文介绍了 Java 内存交互的 8 种基本操做,它们遵循 Java 内存三大特性:原子性、可见性、有序性。
而这三大特性,归根结底,是为了实现多线程的 数据一致性,使得程序在多线程并发,指令重排序优化的环境中能如预期运行。
原子性即一个操做或者多个操做,要么所有执行(执行的过程不会被任何因素打断),要么就都不执行。即便在多个线程一块儿执行的时候,一个操做一旦开始,就不会被其余线程所干扰。
在 Java 中,为了保证原子性,提供了两个高级的字节码指令 monitorenter
和 monitorexit
。这两个字节码,在 Java 中对应的关键字就是 synchronized
。
所以,在 Java 中可使用 synchronized
来保证方法和代码块内的操做是原子性的。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其余线程可以当即看获得修改的值。
JMM 是经过 "变量修改后将新值同步回主内存, 变量读取前从主内存刷新变量值" 这种依赖主内存做为传递媒介的方式来实现的。
Java 实现多线程可见性的方式有:
volatile
synchronized
final
有序性规则表如今如下两种场景: 线程内和线程间
as-if-serial
)的方式执行,此种方式已经应用于顺序编程语言。 synchronized
关键字修饰)以及 volatile
字段的操做仍维持相对有序。 在 Java 中,可使用 synchronized
和 volatile
来保证多线程之间操做的有序性。实现方式有所区别:
volatile
关键字会禁止指令重排序。 synchronized
关键字经过互斥保证同一时刻只容许一条线程操做。 JMM 为程序中全部的操做定义了一个偏序关系,称之为
先行发生原则(Happens-Before)
。先行发生原则很是重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,咱们能够经过几条规则一揽子地解决并发环境下两个操做间是否可能存在冲突的全部问题。
unLock
操做先行发生于后面对同一个锁的 lock
操做。 volatile
变量的写操做先行发生于后面对这个变量的读操做。 Thread
对象的 start()
方法先行发生于此线程的每一个一个动做。 Thread.join()
方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行。 interrupt()
方法的调用先行发生于被中断线程的代码检测到中断事件的发生,能够经过 Thread.interrupted()
方法检测到是否有中断发生。 finalize()
方法的开始。 Java 中如何保证底层操做的有序性和可见性?能够经过内存屏障。
内存屏障是被插入两个 CPU 指令之间的一种指令,用来禁止处理器指令发生重排序(像屏障同样),从而保障有序性的。另外,为了达到屏障的效果,它也会使处理器写入、读取值以前,将主内存的值写入高速缓存,清空无效队列,从而保障可见性。
举个例子:
Store1;
Store2;
Load1;
StoreLoad; //内存屏障
Store3;
Load2;
Load3;
复制代码复制代码
对于上面的一组 CPU 指令(Store 表示写入指令,Load 表示读取指令),StoreLoad 屏障以前的 Store 指令没法与 StoreLoad 屏障以后的 Load 指令进行交换位置,即重排序。可是 StoreLoad 屏障以前和以后的指令是能够互换位置的,即 Store1 能够和 Store2 互换,Load2 能够和 Load3 互换。
常见有 4 种屏障
LoadLoad
屏障 - 对于这样的语句 Load1; LoadLoad; Load2
,在 Load2 及后续读取操做要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。 StoreStore
屏障 - 对于这样的语句 Store1; StoreStore; Store2
,在 Store2 及后续写入操做执行前,保证 Store1 的写入操做对其它处理器可见。 LoadStore
屏障 - 对于这样的语句 Load1; LoadStore; Store2
,在 Store2 及后续写入操做被执行前,保证 Load1 要读取的数据被读取完毕。 StoreLoad
屏障 - 对于这样的语句 Store1; StoreLoad; Load2
,在 Load2 及后续全部读取操做执行前,保证 Store1 的写入对全部处理器可见。它的开销是四种屏障中最大的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。 Java 中对内存屏障的使用在通常的代码中不太容易见到,常见的有 volatile
和 synchronized
关键字修饰的代码块(后面再展开介绍),还能够经过 Unsafe
这个类来使用内存屏障。
volatile
是 JVM 提供的 最轻量级的同步机制。
volatile
的中文意思是不稳定的,易变的,用 volatile
修饰变量是为了保证变量在多线程中的可见性。
volatile
变量具备两种特性:
这里的可见性是指当一条线程修改了 volatile 变量的值,新值对于其余线程来讲是能够当即得知的。而普通变量不能作到这一点,普通变量的值在线程间传递均须要经过主内存来完成。
线程写 volatile 变量的过程:
线程读 volatile 变量的过程:
注意:保证可见性不等同于 volatile 变量保证并发操做的安全性
在不符合如下两点的场景中,仍然要经过枷锁来保证原子性:
- 运算结果并不依赖变量的当前值,或者可以确保只有单一的线程修改变量的值。
- 变量不须要与其余状态变量共同参与不变约束。
可是若是多个线程同时把更新后的变量值同时刷新回主内存,可能致使获得的值不是预期结果:
举个例子: 定义 volatile int count = 0
,2 个线程同时执行 count++ 操做,每一个线程都执行 500 次,最终结果小于 1000,缘由是每一个线程执行 count++ 须要如下 3 个步骤:
具体一点解释,禁止重排序的规则以下:
volatile
变量的读操做或者写操做时,在其前面的操做的更改确定所有已经进行,且结果已经对后面的操做可见;在其后面的操做确定尚未进行; volatile
变量访问的语句放在其后面执行,也不能把 volatile
变量后面的语句放到其前面执行。 普通的变量仅仅会保证该方法的执行过程当中全部依赖赋值结果的地方都能获取到正确的结果,而不能保证赋值操做的顺序与程序代码中的执行顺序一致。
举个例子:
volatile boolean initialized = false;
// 下面代码线程A中执行
// 读取配置信息,当读取完成后将initialized设置为true以通知其余线程配置可用
doSomethingReadConfg();
initialized = true;
// 下面代码线程B中执行
// 等待initialized 为true,表明线程A已经把配置信息初始化完成
while (!initialized) {
sleep();
}
// 使用线程A初始化好的配置信息
doSomethingWithConfig();
复制代码复制代码
上面代码中若是定义 initialized 变量时没有使用 volatile 修饰,就有可能会因为指令重排序的优化,致使线程 A 中最后一句代码 "initialized = true" 在 “doSomethingReadConfg()” 以前被执行,这样会致使线程 B 中使用配置信息的代码就可能出现错误,而 volatile 关键字就禁止重排序的语义能够避免此类状况发生。
具体实现方式是在编译期生成字节码时,会在指令序列中增长内存屏障来保证,下面是基于保守策略的 JMM 内存屏障插入策略:
总结起来,就是“一次写入,处处读取”,某一线程负责更新变量,其余线程只读取变量(不更新变量),并根据变量的新值执行相应逻辑。例如状态标志位更新,观察者模型变量值发布。
JMM 要求 lock、unlock、read、load、assign、use、store、write 这 8 种操做都具备原子性,可是对于 64 位的数据类型(long 和 double),在模型中特别定义相对宽松的规定:容许虚拟机将没有被 volatile
修饰的 64 位数据的读写操做分为 2 次 32 位的操做来进行,即容许虚拟机可选择不保证 64 位数据类型的 load、store、read 和 write 这 4 个操做的原子性。因为这种非原子性,有可能致使其余线程读到同步未完成的“32 位的半个变量”的值。
不过实际开发中,Java 内存模型强烈建议虚拟机把 64 位数据的读写实现为具备原子性,目前各类平台下的商用虚拟机都选择把 64 位数据的读写操做做为原子操做来对待,所以咱们在编写代码时通常不须要把用到的 long 和 double 变量专门声明为 volatile。
咱们知道,final 成员变量必须在声明的时候初始化或者在构造器中初始化,不然就会报编译错误。 final 关键字的可见性是指:被 final 修饰的字段在声明时或者构造器中,一旦初始化完成,那么在其余线程无须同步就能正确看见 final 字段的值。这是由于一旦初始化完成,final 变量的值马上回写到主内存。