Java内存模型精讲

1.JAVA 的并发模型

共享内存模型java

       在共享内存的并发模型里面,线程之间共享程序的公共状态,线程之间经过读写内存中公共状态来进行隐式通讯数组

       该内存指的是主内存,其实是物理内存的一小部分缓存

2.JAVA 内存模型的抽象

2.1 java内存中哪些数据是线程安全的,哪些是非安全的

  1. 非线程安全 : 在 java 中全部的实例域、静态域、和数组元素都存放在堆内存中,而且这些数据是线程共享的,因此会存在内存可见性问题
  2. 线程安全 : 局部变量、方法定义的参数、异常处理器参数是当前线程虚拟机栈中的数据,而且不会进行线程共享,因此不会存在内存可见性问题安全

    2.2 线程间通信的本质

  3. 线程间通信的本质是 :JMM即 JAVA 内存模型进行控制,JMM决定了一个线程对共享变量的写入什么时候对其余线程可见。

图片

由上图能看出来线程间的通信都是经过主内存来进行传递消息的, 每一个线程在进行共享数据处理的时候都是将共享的数据复制到当前线程本地(每一个线程本身都有一个内存)来进行操做。多线程

  1. 消息通信过程(不考虑数据安全性的问题) :
    • 线程一将主内存中的共享变量 A 加载到本身的本地内存中进行处理。好比 A = 1;
    • 此时将修改的共享变量 A 刷入到主内存中, 以后线程二再将主内存中的共享变量 A 读取到本地内存进行操做;

整个数据交互的过程是JMM控制的,主要控制主内存与每一个线程的本地内存如何进行交互来提供共享数据的可见性并发

3.重排序

程序在执行的时候为了提升效率会将程序指令进行从新排序app

3.1 重排序分类

  • 编译器优化重排序

编译器在不改变单线程程序语义的状况下进行语句执行顺序的优化ide

  • 指令集并行重排序

若是不存在数据的依赖性的话,处理器能够改变语句对应机器指令的执行顺序优化

  • 内存系统重排序

因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是在乱序执行线程

3.2 重排序过程

图片

以上三种重排序都会致使咱们在写并发程序的时候出现内存可见性的问题。

JMM的编译器重排序规则会禁止特定类型的编译器重排序;

JMM的处理器重排序规则会要求java编译器在生成指令序列的时候插入特定的内存屏障指令,经过内存屏障指令来禁止特定类型的处理器进行重排序

3.3 处理器重排序

因为为了不处理器等待向内存中写入数据的延时,在处理器和内存中间加了一个缓冲区,这样处理器能够一直向缓冲区中写入数据,等到必定时间将缓冲区的数据一次性的刷入到内存中。

优势 :

  1. 处理器不一样停顿,提升了处理器的运行效率
  2. 减小在向内存写入数据时的内存总线的占用

缺点 :

  1. 每一个处理器上的写缓冲区只对当前处理器可见,因此就会形成内存操做的执行顺序和实际状况不符合

例如如下场景 :

图片

       在当前场景中就可能出如今处理器 A 和处理器 B 没有将它们各自的写缓冲区中的数据刷回内存中, 将内存中读取的A = 0、B = 0 进行给X和Y赋值,此时将缓冲区的数据刷入内存,致使了最后结果和实际想要的结果不一致。由于只有将缓冲区的数据刷入到了内存中才叫真正的执行

       以上主内存与工做内存之间的具体交互协议,即一个变量如何从主内存拷贝到工做内存,如何从工做内存同步到主内存之间的实现细节,JMM定义了如下8种操做来完成

操做 语义解析
lock(锁定) 做用于主内存的变量,把一个变量标记为一条线程独占状态
unlock(解锁) 做用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定
read(读取) 做用于主内存的变量,把一个变量值从主内存传输到线程的工做内存中,以便随后的load动做使用
load(载入) 做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中
use(使用) 做用于工做内存的变量,把工做内存中的一个变量值传递给执行引擎
assign(赋值) 做用于工做内存的变量,它把一个从执行引擎接收到的值赋给工做内存的变量
store(存储) 做用于工做内存的变量,把工做内存中的一个变量的值传送到主内存中,<br>以便随后的write的操做
write(写入) 做用于工做内存的变量,它把store操做从工做内存中的一个变量的值传送<br>到主内存的变量中

       若是要把一个变量从主内存中复制到工做内存中,就须要按顺序地执行read和load操做,若是把变量从工做内存中同步到主内存中,就须要按顺序地执行store和write操做。但Java内存模型只要求上述操做必须按顺序执行,而没有保证必须是连续执行

操做执行流程图解:

图片

同步规则分析

  1. 不容许一个线程无缘由地(没有发生过任何assign操做)把数据从工做内存同步回主内存中
  2. 一个新的变量只能在主内存中诞生,不容许在工做内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操做以前,必须先自行assign和load操做。
  3. 一个变量在同一时刻只容许一条线程对其进行lock操做,但lock操做能够被同一线程重复执行屡次,屡次执行lock后,只有执行相同次数的unlock操做,变量才会被解锁。lock和unlock必须成对出现。
  4. 若是对一个变量执行lock操做,将会清空工做内存中此变量的值,在执行引擎使用这个变量以前须要从新执行load或assign操做初始化变量的值。
  5. 若是一个变量事先没有被lock操做锁定,则不容许对它执行unlock操做;也不容许去unlock一个被其余线程锁定的变量。
  6. 对一个变量执行unlock操做以前,必须先把此变量同步到主内存中(执行store和write操做)

    3.4 内存屏障指令

       为了解决处理器重排序致使的内存错误,java编译器在生成指令序列的适当位置插入内存屏障指令,来禁止特定类型的处理器重排序

内存屏障指令

屏障类型 指令示例 说明
LoadLoadBarriers Load1;LoadLoad;Load2 Load1数据装载发生在Load2及其全部后续数据装载以前
StoreStoreBarriers Store1;StoreStore;Store2 Store1数据刷回主存要发生在Store2及其后续全部数据刷回主存以前
LoadStoreBarriers Load1;LoadStore;Store2 Load1数据装载要发生在Store2及其后续全部数据刷回主存以前
StoreLoadBarriers Store1;StoreLoad;Load2 Store1数据刷回内存要发生在Load2及其后续全部数据装载以前

3.5 happens-before(先行规则)

       happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据

       在JMM中若是一个操做中的结果须要对另外一个操做可见,那么这两个操做以前必需要存在happens-before关系 (两个操做能够是同一个线程也能够不是一个线程)

规则内容:

  • 程序顺序规则 : 指的是在一个线程内控制代码顺序,好比分支、循环等,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行
  • 加锁规则 : 一个解锁(unlock)操做必定要发生于一个加锁(lock)操做以前,也就是说,若是对于一个锁解锁后,再加锁,那么加锁的动做必须在解锁动做以后(同一个锁)
  • volatile变量规则 : 对一个volatile的变量的写操做要发生在对这个变量的读操做以前,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任什么时候刻,不一样的线程老是可以看到该变量的最新值
  • 线程启动规则 : 线程的启动方法 start() 要发生在当前线程全部操做以前
  • 线程终止规则 : 线程中全部的操做都要发生在线程终止以前,Thread.join()方法的做用是等待当前执行的线程终止。假设在线程B终止以前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见
  • 线程中断规则 : 线程调用interrupt()方法要发生在被中断线程的代码检查出中断事件以前
  • 对象终结规则 : 对象的初始化完成要发生在对象被回收以前
  • 传递性规则 : 若是操做 A 发生在操做 B 以前,操做 B 又发生在操做 C 以前,那么操做A必定发生于操做 C 以前

注意: 两个操做之间具备 happens-before 关系,并不意味着前一个操做必需要在后一个操做以前执行,只须要前一个操做的结果对后一个操做可见,而且前一个操做按顺序要排在后一个操做以前。

3.6 数据依赖性

       就是前一个操做的结果对后一个操做的结果产生影响,此时编译器和处理器在处理当前有数据依赖性的操做时不会改变存在数据依赖的两个操做的执行顺序

注意: 此时所说的数据依赖仅仅针对单个处理器中执行的指令序列或者单个线程中执行的操做。不一样处理器和不一样线程的状况编译器和处理器是不会考虑的

3.7 as-if-serial

       在单线程状况下无论怎么重排序程序的执行结果不能被改变,因此若是在单处理器或者单线程的状况下,编译器和处理器对于有数据依赖性的操做是不会进行重排序的。反之若是没有数据依赖性的操做就有可能发生指令重排。

4.数据竞争与顺序一致性

在多线程状况下才会出现数据竞争

4.1 数据竞争

       在一个线程中写了一个变量,在另外一个线程中读一个变量,并且写和读并没有进行同步

4.2 顺序一致性

       若是在多线程条件下,程序可以正确的使用同步机制,那么程序的执行将具备顺序一致性(就像在单线程条件下执行同样) 程序最终运行的结果与你预期的结果同样

4.3 顺序一致性内存模型

4.3.1特性:

  • 一个线程中的全部操做必须按照程序的顺序来执行
  • 全部的操做都必须是原子性的操做,而且对其余线程可见的

4.3.2概念:

       在概念上,顺序一致性有一个单一的全局内存,在任意时间点最多只有一个线程能够链接到内存,当在多线程的场景下,会把全部内存的读写操做变成串行化

4.3.3案例:

       例若有多个并发线程 A B C, A 线程有两个操做 A1 A2, 他们的执行的顺序是 A1 -> A2 。B 线程有三个操做 B1 B2 B3, 他们的执行的顺序是 B1 -> B2 ->B3 。C 线程有两个操做 C1 C2 那么他们在程序中执行的顺序是 C1 -> C2 。

场景分析 :

场景一 : 并发安全(同步)执行顺序

A1 -> A2 -> B1 -> B2 ->B3 -> C1 -> C2

场景二: 并发不安全(非同步)执行顺序

A1 -> B1 -> A2 -> C1 -> B2 ->B3 -> C2

结论 :

       在非同步的场景下,即便三个线程中的每个操做乱序执行,可是在每一个线程中的各自操做仍是保持有序的。而且全部线程都只能看到一个一致的总体执行顺序,也就是说三个线程看到的都是该顺序 : A1 -> B1 -> A2 -> C1 -> B2 ->B3 -> C2 ,由于顺序一致性内存模型中的每一个操做必须当即对任意线程可见。

       以上案例场景在JMM中不是这样的,未同步的程序在JMM中不只总体的执行顺序变了,就连每一个线程的看到的操做执行顺序也是不同的

       例如前面所说的若是线程A将变量的值 a = 2 写入到了本身的本地内存中,尚未刷入到主存中,在线程 A 来看值是变了,可是其余线程 B 线程 C 根本看不到值的改变,就认为线程A 的操做尚未发生,只有线程 A 将工做内存中的值刷回主内存线程 B和线程C 才能的到。可是若是是同步的状况下,顺序一致性模型和JMM模型执行的结果是一致的,可是程序的执行顺序不必定,由于在JMM中,会发生指令重排现象因此执行顺序会不一致

相关文章
相关标签/搜索