深刻讲解并发编程模型之并发三大特性篇

图片描述

推荐阅读

阅读本文以前,建议先阅读 深刻讲解并发编程模型之概念篇 了解什么是重排序、什么是内存屏障、什么是 happens-before。否则下面的内容阅读起来有点费劲。编程


可见性

一个线程的操做结果对其它线程可见成为可见性缓存

  • volatile:保证对变量的写操做的可见性
  • synchronized:对变量的读写(或者)代码块的执行加锁,执行完毕,操做结果写回内存,保证操做的可见性

volatite如何保证可见性

在Java中主要是使用了volatite修饰的变量,那么就能够保证可见效。工做原理以下:多线程

lock前缀指令和MESI协议综合使用

对于volatile修饰的变量,执行写操做的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完以后会当即将这个值写回主内存,同时由于有MESI缓存一致性协议,因此各个CPU都会对总线进行嗅探本身本地缓存中的数据是否被修改了。若是发现某个缓存的值被修改了,那么CPU就会将本身本地缓存的数据过时掉,而后这个CPU上执行的线程在读取那个变量的时候,就会从主内存从新加载最新的数据了。并发

使用lock前缀指令和MESI协议综合使用保证了可见性。app

synchronized如何保证可见性

synchronized主要对变量读写,或者代码块的执行进行加锁,在未释放锁以前,其它现场没法操做synchronized修饰的变量或者代码块。而且,在释放锁以前会讲修改的变量值写到内存中,其它线程进来时读取到的就是新的值了。post

原子性

原子性表示一步操做执行过程当中不容许其余操做的出现,直到该操做的完成。

在Java中,对基本数据类型变量的赋值操做是原子性操做。可是对于复合操做是不具备原子性的,好比:优化

int a = 0; // 具备原子性
a++; // 不具备原子性,这个是复合操做,先读取a的值,再进行+1操做,而后把+1结果写给a
int b = a; // 这个也不具备原子性,先读取a,而后把b值设为a

在Java的JMM模型中,定义了八种原子操做:spa

  • lock(锁定):做用于内存中的变量,将变量标识为某个线程的独占状态
  • unlock(解锁):做用于内存中的变量,将变量从某个线程的独占状态中释放出来,供其它现场获取独占状态
  • read(读取):从内存中读取变量到线程的工做内存中,供load操做使用
  • load(载入):做用于线程工做内存,将read从内存读取的变量,保存到工做内存的变量副本
  • use(使用):做用于工做内存中的变量,当虚拟机执行到须要变量的字节码时,就会须要该动做
  • assign(赋值):做用于工做内存中的变量,当虚拟机执行变量的赋值字节码时,将执行该操做,将值赋值给工做内存中的变量
  • store(存储):做用与工做内存中的变量,将工做内存的变量传递给内存
  • write(写入):做用于内存的变量,将store步骤中传递过来的变量,写入到内存中

有序性

程序执行的顺序按照代码的前后顺序执行代码的执行步骤有序线程

  • valotile:经过禁止指令重排序保证有序性
  • synchronized:经过加锁互斥其它线程的执行保证可见性

在Java中,处理器和编译器会对指令进行重排序的。可是这个重排序只是对单个线程内程序执行结果没有影响,在多线程环境下可能就有影响了。code

int a = 10; // 1
int b = 12; // 2
a = a + 1; // 3
b = b * 2; // 4

实际上,在单线程环境中,程序1和2执行的顺序对程序结果没有影响,程序3和4执行顺序对程序执行结果没有影响,它们是能够在编译器或者处理的优化下作指令重排的,可是程序3不会在程序1以前执行,由于这会影响程序执行结果。具体关于指令重排序,推荐阅读 [深刻讲解并发编程模型之重排序篇
](http://www.funcodingman.cn/po...

boolean flag = true; 
flag = false; // 0
int a = 0;
//线程1执行 一、2 代码
a = 1   // 1
flag = true; // 2    

//线程2执行 三、四、五、6 代码
while(!flag){ // 3
 a = a + 1; // 4
} // 5
System.out.println(a); // 6

此时,若是有两个现场执行该段代码,按照咱们编写的代码逻辑思路是,先执行一、2,再执行三、四、五、六、7。可是在多线程环境中,若是指令进行了重排序,致使2先在0以前执行,那么就会致使预期输出a是2,那么实际是1。

因此,在Java中,咱们须要经过valotite、synchronized对程序进行保护,防止指令重排序让程序输出不是预期的结果。

保证有序性的重要原则

在Java中,编译器和处理器要想对指令进行重排序,若是程序符合下面的原则,就不会发生重排序,这是JMM强制要求的。

happens-before 四大原则

  • 程序次序规则:<span style="color:red">一个线程内</span>(不适用多线程),按照代码顺序,书写在前面的操做先行发生于书写在后面的操做
  • 监视器锁规则:对一个监视器的解锁操做先行发生于后面对同一个锁的占有锁操做
  • volatile变量规则:对一个变量的写操做先行发生于后面对这个变量的读操做。也就是程序代码若是是先写再读,那么就不能重排序先读再写。
  • 传递规则:若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C

若是程序不知足这四大原则的话,原则上是能够任意重排序的。

volatite 如何保证有序性

内存屏障
LoadLoad内存屏障

Load对应JMM中的加载数据的意思。

语法格式:

// load1表示加载指令1,load2表示加载指令2
load1: LoadLoad :load2

LoadLoad屏障:load1;LoadLoad;load2,确保load1数据的装载先于load2后全部装载指令,也就是说,load1对应的代码和load2对应的代码,是不能指令重排的

StoreStore内存屏障

Store对应JMM中存储数据在线程本地工做内存的意思

语法格式:

store1;StoreStore;store2

StoreStore屏障:store1;StoreStore;store2,确保store1的数据必定刷回主存,对其余cpu可见,先于store2以及后续指令

LoadStore内存屏障

语法格式:

load1;LoadStore;store2

LoadStore屏障:load1;LoadStore;store2,确保load1指令的数据装载,先于store2以及后续指令

StoreLoad内存屏障

语法格式:

store1;LoadStore;load2

StoreLoad屏障:store1;StoreLoad;load2,确保store1指令的数据必定刷回主存,对其余cpu可见,先于load2以及后续指令的数据装载

那么volatile修饰的变量,如何在内存屏障中体现的呢?

看一段代码:

volatile a = 1;

a = 2; // store操做

int b = a // load操做

对于volatile修改变量的读写操做,都会加入内存屏障

  • 每一个volatile读操做前面,加LoadLoad屏障,禁止上面的普通读和voaltile读重排
  • 每一个volatile读操做后面,加LoadStore屏障,禁止下面的普通写和volatile读重排
  • 每一个volatile写操做前面,加StoreStore屏障,禁止上面的普通写和它重排
  • 每一个volatile写操做后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排。

因此,上面代码的伪指令代码:

volatile a = 1; // 声明一个a变量,值为1

StoreStore; // 禁止上面的a = 1和a=2重排

a = 2;

StoreLoad; // 确保a的值刷回主内存,对全部CPU可见,下面的读操做才会执行

int b = a;

总结

这里和你们详细分析了并发三大特性问题,分别是可见效、原子性和有序性,以及在Java中如何保证这三大特性,具体的原理是什么。

推荐阅读
相关文章
相关标签/搜索