共享内存模型java
在共享内存的并发模型里面,线程之间共享程序的公共状态,线程之间经过读写内存中公共状态来进行隐式通讯数组
该内存指的是主内存,其实是物理内存的一小部分缓存
线程安全 : 局部变量、方法定义的参数、异常处理器参数是当前线程的虚拟机栈中的数据,而且不会进行线程共享,因此不会存在内存可见性问题安全
由上图能看出来线程间的通信都是经过主内存来进行传递消息的, 每一个线程在进行共享数据处理的时候都是将共享的数据复制到当前线程本地(每一个线程本身都有一个内存)来进行操做。多线程
整个数据交互的过程是JMM控制的,主要控制主内存与每一个线程的本地内存如何进行交互来提供共享数据的可见性并发
程序在执行的时候为了提升效率会将程序指令进行从新排序app
编译器在不改变单线程程序语义的状况下进行语句执行顺序的优化ide
若是不存在数据的依赖性的话,处理器能够改变语句对应机器指令的执行顺序优化
因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是在乱序执行线程
以上三种重排序都会致使咱们在写并发程序的时候出现内存可见性的问题。
JMM的编译器重排序规则会禁止特定类型的编译器重排序;
JMM的处理器重排序规则会要求java编译器在生成指令序列的时候插入特定的内存屏障指令,经过内存屏障指令来禁止特定类型的处理器进行重排序
因为为了不处理器等待向内存中写入数据的延时,在处理器和内存中间加了一个缓冲区,这样处理器能够一直向缓冲区中写入数据,等到必定时间将缓冲区的数据一次性的刷入到内存中。
优势 :
缺点 :
例如如下场景 :
在当前场景中就可能出如今处理器 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内存模型只要求上述操做必须按顺序执行,而没有保证必须是连续执行
操做执行流程图解:
同步规则分析
为了解决处理器重排序致使的内存错误,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及其后续全部数据装载以前 |
happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据
在JMM中若是一个操做中的结果须要对另外一个操做可见,那么这两个操做以前必需要存在happens-before关系 (两个操做能够是同一个线程也能够不是一个线程)
规则内容:
注意: 两个操做之间具备 happens-before 关系,并不意味着前一个操做必需要在后一个操做以前执行,只须要前一个操做的结果对后一个操做可见,而且前一个操做按顺序要排在后一个操做以前。
就是前一个操做的结果对后一个操做的结果产生影响,此时编译器和处理器在处理当前有数据依赖性的操做时不会改变存在数据依赖的两个操做的执行顺序
注意: 此时所说的数据依赖仅仅针对单个处理器中执行的指令序列或者单个线程中执行的操做。不一样处理器和不一样线程的状况编译器和处理器是不会考虑的
在单线程状况下无论怎么重排序程序的执行结果不能被改变,因此若是在单处理器或者单线程的状况下,编译器和处理器对于有数据依赖性的操做是不会进行重排序的。反之若是没有数据依赖性的操做就有可能发生指令重排。
在多线程状况下才会出现数据竞争
在一个线程中写了一个变量,在另外一个线程中读一个变量,并且写和读并没有进行同步
若是在多线程条件下,程序可以正确的使用同步机制,那么程序的执行将具备顺序一致性(就像在单线程条件下执行同样) 程序最终运行的结果与你预期的结果同样
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中,会发生指令重排现象因此执行顺序会不一致。