JAVA内存模型(Happens-Before 规则)
JAVA内存模型由来
1、cpu多核缓存会带来数据的可见性问题
2、编译优化会带来机器指令的有序性问题
前面这两个问题是计算机科学,硬件发展衍生出来的。在提高性能的同时,也引发出对并发编程(共享变量)的一些问题。
- 解决可见性问题最简单的思路是禁用cpu缓存,每次读数据从内存中读取,写数据后就及时刷新到内存中。
- 解决有序性问题最简单的思路是禁用指令重排序。
说白了就是放弃了计算机发展带来的便利,势必会造成性能的大幅下降。可是我们又要保证程序正确,没有并发问题,那我们可以按需禁用,只在存在并发问题的数据上禁用相关的优化。
于是乎出现了 JAVA内存模型(Java Memory Model):
Java 内存模型的本质就是规范了 JVM 如何提供按需禁用缓存和编译优化的方法。
JAVA内存模型包含什么
首先java提供了一系列关键字:synchronized、volitile、final。来保证并发情况下的可见性,原子性,有序性问题。
另外就是Happens-Before 规则,它明确指定了java在一些场景下,前面的操作对于后续的操作一定是可见的,有了这些规则的实现,并发编程就会有迹可循。
主要有以下几种场景:
针对Happens-Before 的几条规则,我也写了几个示例代码:
package jmm;
import org.junit.Test;
public class HappensBefore {
private int shareVal = 1;
private volatile int volatileShareVal = 1;
private final Object mutex = new Object();
/** * @Description 管程中,前一个线程的解锁对后一个线程的加锁是可见的。(如果不能保证可见,锁的竞争就会有问题,也无法保证互斥同步) * @author chenwb * @date 2020/11/3 18:45 */
@Test
public void theLastThreadUnlockAreSeenByNextThreadLock() throws InterruptedException {
Runnable runnable = () -> {
synchronized (mutex) {
shareVal++;
}
};
Thread work1 = new Thread(runnable);
Thread work2 = new Thread(runnable);
work1.start();
work2.start();
work1.join();
work2.join();
}
/** * @Description volatile变量的写操作对后续的读操作是可见的。(禁用缓存,同时加入内存屏障禁止了指令重排序) * @author chenwb * @date 2020/11/3 18:45 */
@Test
public void theOperationAfterWitriingVolitaleValCanSeeVolitaleVal() throws InterruptedException {
Thread child = new Thread(() -> {
for(;;) {
System.out.println(volatileShareVal);
if(volatileShareVal == 77) {
System.out.println(volatileShareVal);
break;
}
}
});
child.start();
// 此处的写对于后续对该变量的读都是可见的
volatileShareVal = 77;
child.join();
}
/** * @Description 主线程启动子线程,在子线程start前面的操作对子线程来说都是可见的。(目前的猜想是禁止重排序) * @author chenwb * @date 2020/11/3 18:45 */
@Test
public void theOperationsBeforeChildThreadStartAreSeenInChildThread() throws InterruptedException {
Thread child = new Thread(() -> System.out.println(shareVal));
shareVal = 77;
// start之前的操作对于子线程是可见的,所以一定打印77
child.start();
// 加join是为了让子线程顺利打印,否则可能主线程会比子线程提前退出
child.join();
}
/** * @Description 主线程启动子线程,子线程调用join方法之后,子线程中的操作对join之后的代码都是可见的。(目前的猜想是禁止重排序) * @author chenwb * @date 2020/11/3 18:45 */
@Test
public void theOperationsBeforeChildThreadJoinAreSeenAfterThreadJoin() throws InterruptedException {
Thread child = new Thread(() -> shareVal = 77);
child.start();
child.join();
// 子线程中的操作对于这里是可见的,所以结果一定是77
System.out.println(shareVal);
}
/** * @Description 线程中断规则:线程发起中断interrupt对于后续的中断检测代码Thread.interrupted()都是可见的。 * @author chenwb * @date 2020/11/3 18:45 */
@Test
public void theInterruptOperationsAreSeenByThreadInterrupted() throws InterruptedException {
Thread child = new Thread(() -> {
// 自旋判断是否中断
for (;;) {
if (Thread.interrupted()) {
// 中断发起之前的操作对于中断检测后是可见的,shareVal == 77(传递性规则)
System.out.println(shareVal);
break;
}
}
});
child.start();
shareVal = 77;
child.interrupt();
child.join();
}
}
小结
1、上面的Happens-Before 规则,我看了字节码的实现,在字节码里面没有什么特殊的字节码。只有对volatile共享变量有标志:
其余的都是在机器指令级别控制的
2、Happens-Before 规则有传递性,比如根据Happens-Before 规则判定A对B是可见的,而B对于C来说是可见的,那么可以判定A对于C来说也是可见的。这一特性在判断可见性问题时是很有用的。
还没有评论,来说两句吧...