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来说也是可见的。这一特性在判断可见性问题时是很有用的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值