前面两个章节我们分享了并发问题产生的根源以及CPU和操作系统提供的解决方案;
通常来讲JVM是运行在操作系统之上,但它本身是一个虚拟机,是可以运行在硬件之上并且提供了一套完备的运行机制,其中就包含了内存模型的定义。
内存模型是什么?
JVM内存模型有两种含义,一种是众所周知的JVM内存的划分,分为了堆、栈、方法区、程序计数器,其中堆又被划分为了新生代和老年代,栈分为虚拟机栈和本地方法栈;堆和方法区是线程共享的,栈和程序计数器是线程私有的。(这里先不细说了,我们将在JVM专题中详细讲解)
内存模型的另一种含义指的是针对前面章节中提到的“数据可见性和缓存一致性”问题而定义的一系列保障程序可以并发并且正常执行的逻辑规则。我们只要合理的运用这些规则和机制,就可以避免并发带来的问题。这一系列规则也就是我们通常说的 “happens-before”规则。
什么是happens-before规则?
通过专栏第一讲的分析,我们知道造成并发问题的根源之一是数据的可见性,举例来说就是CPU1的操作结果不能够及时的被CPU2所知道,CPU2使用了比较旧的数据或者是错误的数据进行运算,最终导致逻辑问题。
happens-before规则就是定义了一系列规则,满足这些规则的时候,先发生的操作的结果一定会被后发生的操作所看到;
happens-before规则如下:(除了规则1之外,其他规则主要是为了解决多线程问题的,所以在思考理解的时候,两个操作要理解为在两个线程中的操作)
1.同一个线程内部,各个操作之间的关系满足happens - before
也就是说在一个线程中,先执行的操作的结果可以被后执行的操作看到;举一个例子,A操作修改了变量a的值,同一个线程中的B操作如果发生在A操作之后,那么B一定可以看到修改之后的值。
但如果B操作发生在另一个线程,则不一定能看到修改后的值。
2.volatile变量的写操作happens-before读操作
对于volatile变量,如果一个写操作发生在读操作之前,那么读操作一定可以看到写操作之后的值。无论读写两个操作是否在一个线程中;
3.解锁操作happens-before加锁操作
解锁操作发生在加锁操作之前,那么加锁操作一定可以看到解锁操作的结果。
本条规则结合规则1和后面的规则7(传递性),就可以正确的完成线程之间的操作可见性。
4.对象构建完成,happens-before finalize操作
对象构建完成的操作(初始化操作)发生在析构之前,那么析构操作一定可以看到初始化的结果。
5.线程的启动,线程的操作,线程终止
这三个操作由happens-before关系,具体来说就是start操作happens线程内的所有操作,线程内的操作happens-before终止操作(比如Thread.join操作,具体含义后面的章节中讲解);
也就是后面的操作一定可以看到前面操作的结果。
6.线程中断的happen-before原则:
对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生(可以通过Thread.interrupted()检测到是否发生中断。)
7.传递性规则
如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
踩坑面试题
这里可能的情况有很多,下面来一一分析:
第一种可能,r1 和 r2都等于0;method1和method2同时执行,分别在第4行和第9行之后中断。
第二种可能,r2 = 0, r1 = 1;先执行method1,后执行method2;
第三种可能,r1 = 0, r2 = 2;先执行method2,后执行method1;
第四种可能,r1 = 1, r2 = 2; 在实际执行时,发生了指令重排序,第4行和第5行交换了位置,第9行和第10行交换了位置;
第五种可能,r1 = 1,r2 = 0;在实际执行时,发生了指令重排序,第4行和第5行交换了位置;结合第一种可能的原因就会得到这个结果。
第六种可能,r1 = 0,r2 = 2;在实际执行时,发生了指令重排序,第9行和第10行交换了位置;结合第一种可能的原因就会得到这个结果。
如果要解决第四、五、六这三种看起来比较诡异的情况,只需要防止指令重排序即可;前面章节已经讲过,Java防止指令重排序的方案是将变量声明volatile。
为什么声明为volatile可以防止重排序,原理是什么,我们将在volatile关键字章节进行讲解。
感谢您的阅读,喜欢请点赞、关注、转发;