JAVA内存模型(Happens-Before 规则)

淡淡的烟草味﹌ 2021-06-10 20:39 468阅读 0赞

JAVA内存模型由来

1、cpu多核缓存会带来数据的可见性问题
2、编译优化会带来机器指令的有序性问题
前面这两个问题是计算机科学,硬件发展衍生出来的。在提高性能的同时,也引发出对并发编程(共享变量)的一些问题。

  • 解决可见性问题最简单的思路是禁用cpu缓存,每次读数据从内存中读取,写数据后就及时刷新到内存中。
  • 解决有序性问题最简单的思路是禁用指令重排序。

说白了就是放弃了计算机发展带来的便利,势必会造成性能的大幅下降。可是我们又要保证程序正确,没有并发问题,那我们可以按需禁用,只在存在并发问题的数据上禁用相关的优化。
于是乎出现了 JAVA内存模型(Java Memory Model):
Java 内存模型的本质就是规范了 JVM 如何提供按需禁用缓存和编译优化的方法。

JAVA内存模型包含什么

首先java提供了一系列关键字:synchronized、volitile、final。来保证并发情况下的可见性,原子性,有序性问题。
另外就是Happens-Before 规则,它明确指定了java在一些场景下,前面的操作对于后续的操作一定是可见的,有了这些规则的实现,并发编程就会有迹可循。
主要有以下几种场景:
在这里插入图片描述
针对Happens-Before 的几条规则,我也写了几个示例代码:

  1. package jmm;
  2. import org.junit.Test;
  3. public class HappensBefore {
  4. private int shareVal = 1;
  5. private volatile int volatileShareVal = 1;
  6. private final Object mutex = new Object();
  7. /** * @Description 管程中,前一个线程的解锁对后一个线程的加锁是可见的。(如果不能保证可见,锁的竞争就会有问题,也无法保证互斥同步) * @author chenwb * @date 2020/11/3 18:45 */
  8. @Test
  9. public void theLastThreadUnlockAreSeenByNextThreadLock() throws InterruptedException {
  10. Runnable runnable = () -> {
  11. synchronized (mutex) {
  12. shareVal++;
  13. }
  14. };
  15. Thread work1 = new Thread(runnable);
  16. Thread work2 = new Thread(runnable);
  17. work1.start();
  18. work2.start();
  19. work1.join();
  20. work2.join();
  21. }
  22. /** * @Description volatile变量的写操作对后续的读操作是可见的。(禁用缓存,同时加入内存屏障禁止了指令重排序) * @author chenwb * @date 2020/11/3 18:45 */
  23. @Test
  24. public void theOperationAfterWitriingVolitaleValCanSeeVolitaleVal() throws InterruptedException {
  25. Thread child = new Thread(() -> {
  26. for(;;) {
  27. System.out.println(volatileShareVal);
  28. if(volatileShareVal == 77) {
  29. System.out.println(volatileShareVal);
  30. break;
  31. }
  32. }
  33. });
  34. child.start();
  35. // 此处的写对于后续对该变量的读都是可见的
  36. volatileShareVal = 77;
  37. child.join();
  38. }
  39. /** * @Description 主线程启动子线程,在子线程start前面的操作对子线程来说都是可见的。(目前的猜想是禁止重排序) * @author chenwb * @date 2020/11/3 18:45 */
  40. @Test
  41. public void theOperationsBeforeChildThreadStartAreSeenInChildThread() throws InterruptedException {
  42. Thread child = new Thread(() -> System.out.println(shareVal));
  43. shareVal = 77;
  44. // start之前的操作对于子线程是可见的,所以一定打印77
  45. child.start();
  46. // 加join是为了让子线程顺利打印,否则可能主线程会比子线程提前退出
  47. child.join();
  48. }
  49. /** * @Description 主线程启动子线程,子线程调用join方法之后,子线程中的操作对join之后的代码都是可见的。(目前的猜想是禁止重排序) * @author chenwb * @date 2020/11/3 18:45 */
  50. @Test
  51. public void theOperationsBeforeChildThreadJoinAreSeenAfterThreadJoin() throws InterruptedException {
  52. Thread child = new Thread(() -> shareVal = 77);
  53. child.start();
  54. child.join();
  55. // 子线程中的操作对于这里是可见的,所以结果一定是77
  56. System.out.println(shareVal);
  57. }
  58. /** * @Description 线程中断规则:线程发起中断interrupt对于后续的中断检测代码Thread.interrupted()都是可见的。 * @author chenwb * @date 2020/11/3 18:45 */
  59. @Test
  60. public void theInterruptOperationsAreSeenByThreadInterrupted() throws InterruptedException {
  61. Thread child = new Thread(() -> {
  62. // 自旋判断是否中断
  63. for (;;) {
  64. if (Thread.interrupted()) {
  65. // 中断发起之前的操作对于中断检测后是可见的,shareVal == 77(传递性规则)
  66. System.out.println(shareVal);
  67. break;
  68. }
  69. }
  70. });
  71. child.start();
  72. shareVal = 77;
  73. child.interrupt();
  74. child.join();
  75. }
  76. }

小结

1、上面的Happens-Before 规则,我看了字节码的实现,在字节码里面没有什么特殊的字节码。只有对volatile共享变量有标志:
在这里插入图片描述
其余的都是在机器指令级别控制的
2、Happens-Before 规则有传递性,比如根据Happens-Before 规则判定A对B是可见的,而B对于C来说是可见的,那么可以判定A对于C来说也是可见的。这一特性在判断可见性问题时是很有用的。

发表评论

表情:
评论列表 (有 0 条评论,468人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java内存模型

           java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种