积跬步至千里

java中 Happens-Before 原则

前言

并发问题有三个根本原因:

  1. cpu 缓存导致可见性问题
  2. 线程切换导致原子性问题:线程切换是发生于任何一条cpu指令级别的,而不是高级语言中的语句,例如 i++ 是三个cpu指令
  3. 编译器优化导致有序性问题

CPU缓存导致可见性问题与Java内存模型(JMM)的问题实际上是两个相互关联的概念。
CPU缓存带来的问题:
在现代多核CPU架构中,每个核心都可能有自己的一级(L1)和二级(L2)缓存,有时甚至有自己的三级(L3)缓存或者共享L3缓存。
当多个CPU核心上的线程分别操作它们各自缓存中的数据副本时,如果没有适当的同步机制,就可能导致一个核心上的线程所做的更改无法及时对其他核心上的线程可见。这就是所谓的"可见性问题"。

java内存模型(JMM):
JMM是Java语言提供的一个抽象概念,用于定义线程如何与主内存以及彼此之间交互。
JMM知道底层硬件(如CPU缓存)可能会导致并发问题,因此它为开发者提供了规则和机制(如volatile关键字、synchronized块、Locks等)来保证在多线程环境下的操作可见性和有序性。
JMM的目标是在保证程序执行效率的同时,提供一种机制来处理由于缓存和其他硬件优化导致的可见性和重排序问题。
因此,虽然可见性问题是由CPU缓存架构导致的,但JMM并不是问题的根源,而是Java提供的一种解决方案。JMM的设计是为了让Java程序员不必直接处理底层硬件的复杂性,而是通过JMM定义的规则和同步机制来解决这些问题。
如果没有JMM,开发者就需要直接处理底层硬件(如CPU缓存)的细节,这会大大增加并发程序的复杂性和出错几率。JMM提供的抽象层允许开发者在编写并发程序时,可以依靠明确的规则来预测并控制线程间的交互。

Happens-Before 原则

在Java内存模型(JMM)中,"Happens-Before" 原则是一组规则,用以确定多线程程序中的内存可见性和操作顺序。这个原则是为了解决并发编程中的两大问题:线程之间的可见性(一个线程对共享变量所做的修改何时对其他线程可见)和指令重排序(编译器和处理器为了优化性能而做的指令乱序执行)。

导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,我们程序的性能可就堪忧了。

合理的方案应该是按需禁用缓存以及编译优化。那么,如何做到“按需禁用”呢?对于并发程序,何时禁用缓存以及编译优化只有程序员知道,那所谓“按需禁用”其实就是指按照程序员的要求来禁用。所以,为了解决可见性和有序性问题,只需要提供给程序员按需禁用缓存和编译优化的方法即可。

Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。

Happens-Before 原则要表达的意思就是前面一个操作的结果对后续操作是可见的

  1. 程序顺序规则:在同一个线程中,按照程序代码顺序,前面的操作Happens-Before于后续的操作。

  2. 管程中锁规则:对一个锁的解锁Happens-Before于随后对这个锁的加锁。

  3. volatile变量规则:对一个volatile域的写操作Happens-Before于任意后续对这个volatile域的读操作。

  4. 传递性:如果操作A Happens-Before操作B,操作B Happens-Before操作C,那么操作A Happens-Before操作C。

  5. 线程启动规则:Thread对象的start()方法Happens-Before于此线程的每一个动作。

  6. 线程终结规则:线程中的所有操作都Happens-Before于线程的终结检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终结。

  7. 线程中断规则:对线程interrupt()的调用happen-before于被中断线程的代码检测到中断事件的发生。

  8. 对象终结规则:一个对象的初始化完成(构造函数执行完成)Happens-Before于它的finalize()方法的开始。

其中最常用的也就是第 1 2 3 4 条

程序顺序规则

这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。比较容易理解。

  int a = 2; // 0
  int b = a + 1; // 1

这里根据 Happens-Before 原则可以保证 1 式中的 a变量值一定为2。看下面的代码,两个语句没有依赖关系,可能发生指令重排,b有可能先于a 赋值

  int a = 2; // 0
  int b = 4; // 1

管程中锁规则

这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。

  synchronized(this){
    if(this.val <10){
	 this.val = 1;
	}
  }

线程 A 执行完代码块中的代码后,val 的值变为1,线程 B 进入代码时,能够看到线程 A对变量 val 的写操作,也就是它能看到 val == 1;

volatile变量规则

这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作,这块其实就是禁用缓存的意思。

  • 当一个线程对volatile变量进行写操作时,该值会立即被写入主内存
  • 当一个线程对volatile变量进行读操作时,它会从主内存中读取在最新的值到工作内存
    private static volatile boolean flag;

    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        new Thread(() -> {
            while (!flag) {

            }
            countDownLatch.countDown();
            System.out.println("hello world");
        }, "thread 1").start();
        System.out.println("main thread will change the variable after 3 second");
        IntStream.rangeClosed(1,3).forEach(e -> {
            try {
                System.out.println(e + "second");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ex) {
                throw new RuntimeException(ex);
            }
        });
        flag = true;
        countDownLatch.await();
    }

传递性

这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。

 volatile flag;
  // 线程 A 
  a = 12; // 写变量
  flag = true; // 写变量
  // 线程 B
  flagb = flag; // 读变量
  x = a ; // 读变量
  1. a = 12; Happens-Before 写变量 flag = true;,这是程序顺序规则
  2. 写变量 flag = true; Happens-Before 读变量 flagb = flag;,这是 volatile 变量规则
    根据传递性规则,我们得到 a = 12; Happens-Before 读变量 flagb = flag;。即线程B 读到 flag == true;,那么线程A 设置的a =12 对于线程B 是可见的。
posted @ 2024-01-15 19:50  大阿张  阅读(50)  评论(0编辑  收藏  举报