这篇文章我们来讲一下JMM和其相关的内容。
目录
1.JMM模型的介绍
2.volatile的底层原理
3.有序性的介绍
3.1as-if-serial原则
3.2happen-before原则
4.内存屏障
5.小结
1.JMM模型的介绍
首先,我们来看一下JMM模型。
这是一张多核CPU的并发缓存架构图。我们的数据存在主内存RAM中,由于CPU的运算速度非常快,而CPU从主内存中读取数据的速度比较慢(与前者的速度是差几个量级的),所以为了适配这二者的速度差异,我们在CPU中开辟了一块缓存区,空间不大,里面放的是CPU中使用频率较高的数据,CPU从缓存区中读取数据的速度就比从主内存中读取数据的速度要快的多,这样就便于我们CPU的运行。我们的JMM模型就与上面的多核CPU并发缓存架构类似。
Java多线程内存模型(简称JMM)跟cpu缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
如下图所示:
下面举例来解释一下:
假设主内存中有一个boolean类型的变量flag,初始值为true,现在我们的线程1要将这个flag改为false,它会先把这个flag复制一份到线程1的工作内存,然后在工作内存中将这个flag改为false。此时线程2和线程3中不一定会感知到这个flag为false。也就是说,我们的线程1将flag改为了false,但是我们的线程2和线程3中的flag还是true。这就是不满足线程的可见性。
下面我们来看一下程序:
解释一下:
首先是定义了一个共享变量initFlag,初始值为false,然后是main方法,里面new了一个线程,线程里面打印一句话,然后是一个死循环,然后线程启动。然后是主线程睡眠2s,然后又new了一个线程,线程里面调用一个方法,方法里面打印一句话,然后修改initFlag的值,然后再打印一句话。正常情况下,initFlag值被修改后,线程1中的死循环会跳出来,会打印success那句话。
但是结果结果显然不是这样的,根据结果我们可以知道,线程2中的所有内容都执行完了,但是线程1中的死循环还没有结束,那句success还没打印出来。那就说明线程2修改后的initFlag值没有被线程1感知到,所以线程1中的死循环没有结束。这就符合我们上面的JMM的讲解了。
那怎么解决呢?给我们的共享变量加一个volatile即可!
如下图所示:
这样问题就解决了
2.volatile的底层原理
上面我们讲了volatile可以解决可见性的问题,下面我们来看一下volatile的底层原理。
在讲volatile的底层原理之前,我们先来了解一下JMM的数据原子操作
如下图所示:
下面通过一个具体的例子来讲解一下
如下图所示(例子是上面initFlag的例子):
首先,主内存中存了 变量initFlag,初始值为false,然后线程1通过总线读取到initFlag,即read操作,然后是load操作,将initFlag写入工作内存中,然后是use操作,对应程序中就是进行判断。同一时刻,线程2也在进行这些操作,不过对应到程序中,线程2的use操作就是改值,然后线程2进行assign赋值操作,将新的initFlag值赋值到线程2的工作内存中的变量中,此时线程2中的initFlag才变为true,然后是store存储操作,即线程2将工作内存中的initFlag值存入主内存中,注意,此时主内存中原本的initFlag值还依然为false,等到最后一步write写入操作,才将主内存中的initFlag值改为true。
但是在线程2进行后面的一系列操作时,线程1中的initFlag值始终为false,并且线程1始终在使用这个initFlag的值,这就是不可见性。
那volatile到底是怎么保证我线程2在修改完initFlag值的同时,我线程1也能感知到并及时修改的呢?
首先,我们来了解两点内容:
然后,我们来看一张图,然后来解释一下:
首先说明一点,这个缓存一致协议是硬件上面的内容。
它的流程是这样的:当我们的线程2修改了initFlag的值之后,也就是执行了assign赋值操作后,它会瞬间触发后面的store存储和write写入这两个操作,也就是说,当某个CPU修改了工作内存里面的数据后,它会马上就将数据同步到主内存中。而其他的CPU通过总线嗅探机制会感知到自己缓存中数据的变化,然后将自己缓存中的数据判为无效数据,然后再重新从主内存中拿数据。
下面了解一下缓存一致协议(了解即可):
那volatile到底是怎么实现上面的那一套功能的呢?
我们来看下面的这张图:
简单来说就是:volatile的底层实现上会有一个汇编的lock前置指令,而这个汇编的lock前置指令会实现硬件层级的缓存一致协议,而缓存一致协议就是那些巴拉巴拉......的东西了
3.有序性的介绍
下面介绍并发三大特性中的有序性。
如下图所示:
简单来说就是:一般情况下,我们的程序是按照我们所写的每一行代码的顺序来运行的,但是有时候,为了提供程序的运行效率,计算机会将我们所写的代码编译为汇编语言后,改变我们所写代码是顺序,然后再运行,这也叫指令重排序,这就会导致在并发的情况下出现错误。
3.1as-if-serial原则
as-if-serial语义:
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-seriali语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
3.2happen-before原则
只靠sychronized和volatle关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,从JDK5开始,Java使用新的JSR-133内存模型,提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据。
happens-before原则内容如下
- 程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
- 锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
- volatile规则:volatile变量的写,先发生于读,这保证了volatle变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
- 线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
- 传递性:A先于B,B先于C那么A必然先于C
- 线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
- 对象终结规则:对象的构造函数执行,结束先于finalize()方法
4.内存屏障
下面讲一下Java语言规范的内存屏障。
什么是内存屏障?
简单来说就是,如果这两行代码之间可能发生指令重排序,但是你不想让他们发生指令重排序,那么你就需要在这两行代码之间加上一行代码来防止它们进行指令重排序。加的这行代码就是内存屏障。
内存屏障是什么样的?
如下图所示:
其中的Load、store是Java内存模型的数据原子性操作。
怎么用这个内存屏障?
这个不用你操心,volatile已经帮你用好了。volatile的底层实现上是会有一个汇编的lock前缀,而这个lock前缀就已经实现了内存屏障。
5.小结
这篇文章我们主要讲了JMM,即Java内存模型,讲了JMM数据的原子性操作,讲了volatile的底层实现原理,讲了缓存一致协议,讲了有序性,讲了有序性的两大规则,讲了内存屏障。
内容很散,需要理解,需要自己把这些散的内容串起来。