一篇文章看懂Java并发和线程安全(二)

上一篇博文《一篇文章看懂Java并发和线程安全(一)》讲述了多线程中,程序总不能按照我们所看到的那样执行,必须保证共享数据的可见性和执行临界区代码的有序性,才能让多线程程序运行成我们想要的样子,本篇博客将继续深入讲解一个有序而又乱序的Java世界。

工作内存与主内存的数据交换的细节

volatile、final、锁的内存语义

进入细节

JMM定义了8种基本操作来完成,主内存、工作内存和执行引擎之间的交互,分别是lock、unlock、read、load、use、assign、store、write,虚拟机的实现向程序员保证每一种操作都是原子的,不可分割,对于double和long类型的64为变量不做保证。了解了这些,有助于帮我们理解内存屏障。

针对于主内存的单独操作lock和unlock

·unlock:作用于主内存、释放锁定状态

·read:作用于主内存,把主内存变量传递给工作内存

工作内存到主内存的写交换

·write:作用于主内存,把store过来的值写入主内存变量

·use:作用于工作内存,把工作内存变量传递给执行引擎

上述的交互关系,可以用如下的图来表示:

总体来说,工作内存和主内存的数据交换读写都是用两组操作来完成,而执行引擎和工作内存的数据交换由两个操作完成。当然,上述的8种操作必须满足一些规则,这里列举一些我认为重要的,例如:

·对变量执行lock操作,会清空工作内存中缓存的该值,对变量执行unlock操作,必须先把值同步回主内存。

乱序的Java世界

站在B的视角看,它看不清a=1和b=1谁先执行,由于指令重排序,很可能b=1先执行,请看下表:

站在B线程的视角,B线程中read方法里的代码是否会重排序呢,虽然这个方法的两句话存在依赖关系,JMM支持不改变结果的指令重排,JMM无法预先判断是否有其他线程在修改a的值,所以可能会重排,并且处理器会用猜测执行来重排。请看下表:

指令重排序让线程看不清对方线程的执行顺序,也就是乱序的,那么会有哪些级别的指令重排序呢?有三种:编译器重排序、指令级重排序、内存级重排序。

指令重排序会导致多线程执行的无序,那么JMM会禁止特定类型的指令重排序,JMM通过内存屏障来禁止某些指令重排序,那么有哪些内存屏障呢?总共4类

·StoreStore:前面的store会先于后面的store执行,也就是保证内存可见性

·StoreLoad:前面的store先于后面的Load执行

volatile

·对volatile变量的读操作,后面会插入两个屏障,分别是LoadLoad、LoadStore,说白了就是,我是volatile变量,不管你下面的变量是读或者写,我都要先于你读。

final本质上定义是final域与构造对象的引用之间的内存屏障。

读含有final变量的对象的引用,与读final变量不能指令重排序,插入loadload屏障,保证先读到对象引用,在读final变量的值,也就是只要对象构造完成,并且在构造函数中将final值写入,另外一个线程肯定可以读到,这是JMM的保证。

ReentrantLock中 有个private volatile int state,本质上是用的volatile的内存语义,这里就省略讲了。

前面说这么多,指令重排序重排序,弄乱了Java程序,JMM提供volatile、final、锁来禁止某些指令重排序,那么记住这些重排序规则并非简单的事,JMM用另外一种好记的理论来帮助程序员记忆。

int i=1;int j=2;int a=i*j;

happens-before:happens-before保证如果A、B两个操作存在happens before关系,那么A操作的结果一定对B可见,有了可见性的保证,在加上正确的同步,就能写出线程安全的代码。JSR133定义了哪些天然的happens-before关系呢?请看下面:

·unlock操作happens-before对这个这个锁的lock操作

·线程的start方法happens-before此线程的所有其他操作

·传递性,A happens-before B、B happens-before C,那么A happens-before C

最后还有两句话

一个线程看另一个线程,所有的操作都是无序的,主要是两方面所致,一方面是指令重排序,另一方面是不知道工作内存的值什么时候同步到主内存。

注:关注作者微信公众号,了解更多分布式架构、微服务、netty、MySQL、spring、、性能优化、等知识点。

公众号:《 Java大蜗牛 》

你可能感兴趣的:(一篇文章看懂Java并发和线程安全(二))