Happens-Before原则深入解读

 2023-01-14
原文作者:转转技术团队 原文地址:https://juejin.cn/post/7124504859247804424

Happens-Before(先行发生)原则是对Java内存模型(JMM)中所规定的可见性的更高级的语言层面的描述。用这个原则解决并发环境下两个操作之间的可见性问题,而不需要陷入Java内存模型苦涩难懂的定义中。关于Java内存模型中所规定的可见性定义本文不再叙述,感兴趣的读者可参考的书籍有《深入理解Java虚拟机》和《Java并发编程的艺术》。

1 Happens-Before(先行发生)原则的定义

  • 程序次序规则(Program Order Rule) :在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 管程锁定规则(Monitor Lock Rule) :一个unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile变量规则(Volatile Variable Rule) :对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则(Thread Start Rule) :Thread对象start()方法先行发生于此线程的每一个动作。
  • 线程终止规则(Thread Termination Rule) :线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法和Thread.isAlive()的返回值等手段检测线程是否已经终止执行。
  • 线程中断规则(Thread Interruption Rule) :对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule) :一个对象的初始化完成(构造函数结束)先行发生于它的finalize()方法的开始。
  • 传递性(Transitivity) :如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

Happens-Before原则最难以理解的地方在于如何理解"Happens-Before(先行发生)"这个词。我们以程序次序规则为例,“ 书写在前面的操作先行发生于书写在后的操作 ”,如果理解为“ 书写在前面的操作比书写在后面的操作先执行 ”看起来是没有什么问题的,写在前面的操作确实在程序逻辑上比写在后面的操作先执行。按照同样的理解,我们看一下管程锁定规则,“ unlock操作先行发生于后面对同一个锁的lock操作 ”,如果理解为“ unlock操作比同一个锁的lock操作先执行 ”这就很困惑了,还没有加锁,怎么解锁。

之所出现这种困惑的解读方式,是因为把“先行发生”理解为一种 主动的规则要求 了,而“先行发生”事实上是程序运行时出现的 客观结果 。正确的解读方式是这样的,对于“ 同一把锁 ”, 如果 在程序运行过程中“一个unlock操作先行发生于同一把锁的一个lock操作”, 那么 “该unlock操作所产生的影响(修改共享变量的值、发送了消息、调用了方法)对于该lock操作是可见的”。

按照这种理解,依次重新解读其他规则。

  • 程序次序规则 :在一个线程内,按照控制流顺序,如果操作A先行发生于操作B,那么操作A所产生的影响对于操作B是可见的。
  • 管程锁定规则 :对于同一个锁,如果一个unlock操作先行发生于一个lock操作,那么该unlock操作所产生的影响对于该lock操作是可见的。
  • volatile变量规则 :对于同一个volatile变量,如果对于这个变量的写操作先行发生于这个变量的读操作,那么对于这个变量的写操作所产的影响对于这个变量的读操作是可见的。
  • 线程启动规则 :对于同一个Thread对象,该Thread对象的start()方法先行发生于此线程的每一个动作,也就是说对线程start()方法调用所产生的影响对于该该线程的每一个动作都是可见的。
  • 线程终止规则 :对于一个线程,线程中发生的所有操作先行发生于对此线程的终止检测,也就是说线程中的所有操作所产生的影响对于调用线程Thread.join()方法或者Thread.isAlive()方法都是可见的。
  • 线程中断规则 :对于同一个线程,对线程interrupt()方法的调用先行发生于该线程检测到中断事件的发生,也就是说线程interrupt()方法调用所产生的影响对于该线程检测到中断事件是可见的。
  • 对象终结规则 :对于同一个对象,它的构造方法执行结束先行发生于它的finalize()方法的开始,也就是说一个对象的构造方法结束所产生的影响,对于它的finalize()方法开始执行是可见的。
  • 传递性 :如果操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C,也就说操作A所产生的所有影响对于操作C是可见的。

2 示例代码

2.1 管程锁定规则

2.1.1 WithoutMonitorLockRule

如下的这段代码开启了两个线程,updater和getter,updater线程将stop变量设置为true,getter线程死循环判断stop变量为true时结束循环。运行这段程序,得到以下输出。

updater set stop true.

getter线程未输出getter stopped,说明updater线程对stop变量的修改对getter线程不可见。

    public class WithoutMonitorLockRule {
        private static boolean stop = false;
    
        public static void main(String[] args) {
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sleep(1000);
                    stop = true;
                    System.out.println("updater set stop true");
                }
            }, "updater");
    
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        if (stop) {
                            System.out.println("getter stopped");
                            break;
                        }
                    }
                }
            }, "getter");
            getter.start();
            updater.start();
        }
    
    }

2.1.2 MonitorLockRuleSynchronized

如下的这段代码定义了变量lockObject作为同步锁,运行这段程序,得到以下输出。

updater set stop true.
getter stopped.

该输出表明updater线程对stop变量的修改对getter线程是可见的。结合Happens-Before原则进行分析,根据 程序次序规则 在updater线程内stop = true先行发生于lockObject锁的释放,在getter线程内lockObject锁的获取先行发生于if (stop);再根据 传递性stop = true先行发生于if (stop),所以stop = true对于if (stop)是可见的。

    public class MonitorLockRuleSynchronized {
        private static boolean stop = false;
        private static final Object lockObject = new Object();
    
        public static void main(String[] args) {
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sleep(1000);
                    synchronized (lockObject) {
                        stop = true;
                        System.out.println("updater set stop true.");
                    }
                }
            }, "updater");
    
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        synchronized (lockObject) {
                            if (stop) {
                                System.out.println("getter stopped.");
                                break;
                            }
                        }
                    }
                }
            }, "getter");
            updater.start();
            getter.start();
        }
    }

2.1.3 MonitorLockRuleReentrantLock

ReentrantLock也可以起到和Synchronized关键字同样的效果,在Lock接口的注释中有如下描述。这段描述的意思是说所有的Lock接口实现必须在内存可见性上具有和内置监视器锁(Synchronized)相同的语义。

    Memory Synchronization
    All Lock implementations must enforce the same memory synchronization semantics as provided by the built-in monitor lock, as described in The Java Language Specification (17.4 Memory Model) :
    A successful lock operation has the same memory synchronization effects as a successful Lock action.
    A successful unlock operation has the same memory synchronization effects as a successful Unlock action.

使用ReentrantLock编写这段代码如下,同样可以实现和Synchronized相同的效果,具体原理在接下来的volatile变量规则中讨论。

    public class MonitorLockRuleReentrantLock {
        private static boolean stop = false;
    
        private static ReentrantLock reentrantLock = new ReentrantLock();
    
        public static void main(String[] args) {
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sleep(1000);
                    reentrantLock.lock();
                    try {
                        stop = true;
                        System.out.println("updater set stop true.");
                    } finally {
                        reentrantLock.unlock();
                    }
                }
            }, "updater");
    
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        reentrantLock.lock();
                        try {
                            if (stop){
                                System.out.println("getter stopped.");
                                break;
                            }
                        }finally {
                            reentrantLock.unlock();
                        }
                    }
                }
            }, "getter");
            updater.start();
            getter.start();
        }
    
    }

2.2 volatile变量规则

2.2.1 WithoutVolatileRule

如下的这段代码开启了两个线程,updater和getter,updater线程将stop变量设置为true,getter线程死循环判断stop变量为true时结束循环。运行这段程序,得到以下输出。

updater set stop true.

getter线程未输出getter stopped,说明updater线程对stop变量的修改对getter线程不可见。

    public class WithoutVolatileRule {
        private static boolean stop = false;
    
        public static void main(String[] args) {
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sleep(1000);
                    stop = true;
                    System.out.println("updater set stop true");
                }
            }, "updater");
    
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        if (stop) {
                            System.out.println("getter stopped");
                            break;
                        }
                    }
                }
            }, "getter");
            getter.start();
            updater.start();
        }
    }

2.2.2 VolatileRule

使用volatile关键字修饰stop变量之后运行这段程序得到以下输出。

updater set stop true.
getter stopped.

说明updater线程对stop的修改对于getter线程可见。使用Happens-Before原则进行分析,根据 volatile变量规则 ,updater线程对stop变量的写操作先行发生于getter线程对stop变量的读操作,所以updater线程将stop变量设置为true对getter线程读取stop变量是可见的。

    public class VolatileRule {
        private static volatile boolean stop = false;
    
        public static void main(String[] args) {
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sleep(1000);
                    stop = true;
                    System.out.println("updater set stop true.");
                }
            }, "updater");
    
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        if (stop) {
                            System.out.println("getter stopped.");
                            break;
                        }
                    }
                }
            }, "getter");
            updater.start();
            getter.start();
        }
    }

2.2.3 VolatileRule1

volatile变量还有一个说是特点,其实也不是的特性。如下代码并未将stop变量用volatile修饰,而是用volatile修饰了volatileObject变量。运行这段代码将得到如下输出。

updater set stop true.
getter stopped.

    public class VolatileRule1 {
        private static  boolean stop = false;
        private static volatile Object volatileObject = new Object();
    
        public static void main(String[] args) {
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sleep(1000);
                    stop = true;
                    volatileObject = new Object();
                    System.out.println("updater set stop true.");
                }
            }, "updater");
    
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        Object volatileObject = VolatileRule1.volatileObject;
                        if (stop) {
                            System.out.println("getter stopped.");
                            break;
                        }
                    }
                }
            }, "getter");
            updater.start();
            getter.start();
        }
    
    }

上述结果表明虽然stop变量未被volatile修饰,但是它仍然在updater线程和getter线程之间可见,在未仔细品读Happens-Before原则之前,仅仅从java语法上来看是很神奇的。

结合Happens-Before原则的 程序次序规则传递性 进行仔细分析。updater线程在将stop变量设置为true之后,又对volatileObject变量进行了赋值,而getter线程在读取stop变量之前首先读取了volatileObject。根据 程序次序规则 在updater线程内stop = true先行发生于volatileObject = new Object(),在getter线程内Object volatileObject = VolatileRule1.volatileObject先行发生于if (stop);再根据 传递性stop = true先行发生于if (stop),所以stop = true对于if (stop)就是可见的。

如此分析之后便发现这个特性并不神奇,仅仅是 传递性 在起作用罢了,对于其他Happens-Before原则这个特性同样存在,读者可自行验证。利用此特性的一个典型例子在jdk的java.util.concurrent.FutureTask中,如下的代码节选自FutureTask,在outcome后面的注释上写着non-volatile, protected by state reads/writes,而state是被volatile所修饰的。这就是为什么在使用线程池的submit方法向线程池提交任务时,执行结果是可见的。而使用execute方法向线程池提交任务,这个任务所做的修改却不可见。读者可以自行验证。

    private volatile int state;
    /** The result to return or exception to throw from get() */
    private Object outcome; // non-volatile, protected by state reads/writes

上文提到的ReentrantLock对管程锁定规则的保证同样和volatile变量有关,在java.util.concurrent.locks.AbstractQueuedSynchronizer类中有state属性,该属性被volatile关键字修饰,不再赘述。

2.3 Thread Start Rule

2.3.1 WithoutThreadStartRule

如下的代码首先开启了updater线程,updater线程调用getter.start开启了getter线程,随后updater线程将stop设置为true,getter线程死循环判断stop变量为true时结束循环。运行该程序得到如下输出。

updater set stop true.

getter线程未输出getter stopped,说明updater线程对stop变量的修改对getter线程不可见。

    public class WithoutThreadStartRule {
        private static boolean stop = false;
    
        public static void main(String[] args) {
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true){
                        if (WithoutThreadStartRule.stop) {
                            System.out.println("getter stopped.");
                            break;
                        }
                    }
                }
            }, "getter");
    
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    getter.start();
                    Utils.sleep(1000);
                    stop = true;
                    System.out.println("updater set stop true.");
                    while (true){
                    }
                }
            });
            updater.start();
        }

2.3.2

接下来对调一下updater线程中getter.start()stop = true的位置,如下代码所示。运行该程序得到如下输出。

updater set stop true.
getter stopped.

该输出表明updater线程对stop变量的修改对getter线程是可见的。结合Happens-Before原则来分析一下,根据 程序次序规则 在updater线程内stop = true先行发生于getter.start();而根据 线程启动规则 getter.start() 先行发生于该线程内的每一个动作(包括if (stop));再根据 传递性 规则stop = true先行发生于if (stop),所以stop = trueif (stop)是可见的。

    public class ThreadStartRule {
        private static boolean stop = false;
    
        public static void main(String[] args) {
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true){
                        if (stop) {
                            System.out.println("getter stopped.");
                            break;
                        }
                    }
                }
            }, "getter");
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    stop = true;
                    System.out.println("updater set stop true.");
                    Utils.sleep(1000);
                    getter.start();
                    while (true){
                    }
                }
            });
            updater.start();
        }
    }

2.4 线程终止规则

2.4.1 WithoutThreadTermination

如下的这段代码开启了两个线程,updater和getter,updater线程将stop变量设置为true,getter死循环判断stop变量为true时结束循环。运行该程序得到以下输出。

updater set stop true.

getter线程未输出getter stopped,说明updater线程对stop变量的修改对getter线程不可见。

    public class WithoutThreadTerminationRule {
        private static boolean stop = false;
    
        public static void main(String[] args) throws InterruptedException {
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sleep(1000);
                    stop = true;
                    System.out.println("updater set stop true.");
                }
            });
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true){
                        if (stop) {
                            System.out.println("getter stopped.");
                            break;
                        }
                    }
                }
            }, "getter");
            updater.start();
            getter.start();
        }
    }

2.4.2 ThreadTerminationRule

如下的代码getter线程在获取stop变量之前调用了updater.join()等待updater线程结束。运行这段代码得到以下输出。

updater set stop true.
getter stopped.

该输出结果表明updater线程对stop变量的修改对getter线程是可见的。结合Happens-Before原则进行分析,根据 线程终止规则 updater线程中的所有操作(包括stop = true)先行发生于getter线程调用updater.join()等待updater结束;根据 程序次序规则 在getter线程内updater.join()先行发生于if (stop);再根据 传递性 得出stop = true先行发生于if (stop),所以stop = trueif (stop)是可见的。

    public class ThreadTerminationRule {
        private static boolean stop = false;
    
        public static void main(String[] args) throws InterruptedException {
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sleep(1000);
                    stop = true;
                    System.out.println("updater set stop true.");
                }
            });
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        updater.join();
                    } catch (InterruptedException e) {
    
                    }
                    while (true){
                        if (stop) {
                            System.out.println("getter stopped.");
                            break;
                        }
                    }
                }
            }, "getter");
            updater.start();
            getter.start();
        }
    }

2.5 线程中断规则

2.5.1 WithoutThreadInterruptRule

如下的这段代码开启了两个线程,updater和getter,updater线程将stop变量设置为true,getter死循环判断stop变量为true时结束循环。运行这段代码得到以下输出。

updater set stop true.

getter线程未输出getter stopped,说明updater线程对stop变量的修改对getter线程不可见。

    public class WithoutThreadInterruptRule {
        private static boolean stop = false;
    
        public static void main(String[] args) {
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        if (stop) {
                            System.out.println("getter stopped.");
                            break;
                        }
                    }
                }
            }, "getter");
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sleep(1000);
                    stop = true;
                    System.out.println("updater set stop true.");
                }
            }, "updater");
    
            updater.start();
            getter.start();
        }
    }

2.5.2 ThreadInterruptRule

如下的这段代码updater线程在将stop设置为true之后,调用了getter.interrupt()方法,而getter线程在获取stop变量的值之前首先判断了Thread.currentThread().isInterrupted(),运行这段代码得到以下输出。

updater set stop true.
getter stopped.

该输出结果表明updater线程对stop变量的修改对getter线程是可见的。根据Happens-Before原则进行分析,根据 程序次序规则 在updater线程内stop = true先行发生于getter.interrupt(),在getter线程内Thread.currentThread().isInterrupted() 先行发生于if (stop);根据 线程中断规则 getter.interrupt()先行发生于Thread.currentThread().isInterrupted();再根据 传递性 得出stop = true先行发生于if (stop),所以stop = true对于if (stop)是可见的。

    public class ThreadInterruptRule {
        private static boolean stop = false;
    
        public static void main(String[] args) {
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        if (Thread.currentThread().isInterrupted()) {
                            if (stop) {
                                System.out.println("getter stopped.");
                                break;
                            }
                        }
                    }
                }
            }, "getter");
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sleep(1000);
                    stop = true;
                    getter.interrupt();
                    System.out.println("updater set stop true.");
                }
            }, "updater");
    
            updater.start();
            getter.start();
        }
    }

2.6 对象终结规则

2.6.1 FinalizerRule

如下的代码中新建了一个Test对象,在Test对象的构造方法中将stop变量设置为true,随后将test变量赋值为null,则新建的Test对象成为垃圾,再在死循环中分配对象促使垃圾回收。运行该程序得到以下输出。

set stop true in constructor
stop true in finalize, threadName Finalizer

根据 程序次序规则 在main线程中stop = true先行发生于Test的构造方法结束;根据 对象终结规则 Test的构造方法结束先行发生于Test的finalize()方法的开始;再根据 传递性 得出stop = true先行发生于finalize()方法的开始,所以stop = true对于finalize()方法是可见的。

    public class FinalizerRule {
        private static boolean stop = false;
    
        public static void main(String[] args) {
            Test test = new Test();
            //test设置为null以后就可以回收了
            test = null;
            while (true) {
                //促使垃圾回收
                byte[] bytes = new byte[1024 * 1024];
            }
        }
    
        static class Test {
            public Test() {
                stop = true;
                System.out.println("set stop true in constructor");
            }
    
            @Override
            protected void finalize() throws Throwable {
                if (stop) {
                    System.out.println("stop true in finalize, threadName  " + Thread.currentThread().getName());
                } else {
                    System.out.println("stop false in finalize, threadName " + Thread.currentThread().getName());
                }
            }
        }
    }

对象终结规则的反例特别难以复现,而对象终结规则在编程过程中又很难接触到,所以此处不再举例。

3 进一步解读

在第2节中很多例子的可见性保证都结合了 程序次序规则传递性 ,据此对前几条规则进行进一步解读如下。

  • 管程锁定规则 :对于同一个锁,如果一个unlock操作先行发生于一个lock操作,那么该unlock操作( 包括unlock操作之前的操作 )所产生的影响对于该lock操作( 包括lock操作之后的操作 )是可见的。
  • volatile变量规则 :对于同一个volatile变量,如果对于这个变量的写操作先行发生于对于这个变量的读操作,那么对于这个变量的写操作( 包括写操作之前的操作 )所产生的影响对于这个变量的读操作( 包括读操作之后的操作 )是可见的。
  • 线程启动规则 :对于同一个Thread对象,该Thread对象的start()方法先行发生于此线程的每一个动作,也就是说对线程start()方法调用( 包括start方法之前的操作 )所产生的影响对于该该线程的每一个动作都是可见的。
  • 线程终止规则 :对于一个线程,线程中发生的所有操作先行发生于对此线程的终止检测,也就是说线程中的所有操作所产生的影响对于调用线程的Thread.join()方法或者Thread.isAlive()方法( 包括调用这两个方法之后的操作 )都是可见的。
  • 线程中断规则 :对于同一个线程,对线程interrupt()方法的调用先行发生于该线程检测到中断事件的发生,也就是说线程interrupt()方法调用( 包括interrupt方法调用之前的操作 )所产生的影响对于该线程检测到中断事件( 包括检测到中断事件之后的操作 )是可见的。
  • 对象终结规则 :对于同一个对象,它的构造方法执行结束先行发生于它的finalize()方法的开始,也就是说一个对象的构造方法结束( 包括构造方法结束前的操作 )所产生的影响,对于它的finalize()方法开始执行( 包括开始之后的操作 )是可见的。

对于该进一步解读,以管程锁定规则为例,如下代码并未将stop = trueif (stop)包括在syncronized代码块内,但是在updater线程内stop = true在sychronized代码块之前,而在getter线程内if (stop)在syncronized代码块之后。运行该程序得到以下输出。

updater set stop true.
getter stopped.

说明updater线程对stop的修改对getter线程是可见的。

    public class MonitorLockRuleSynchronized1 {
        private static boolean stop = false;
        private static final Object lockObject = new Object();
    
        public static void main(String[] args) {
            Thread updater = new Thread(new Runnable() {
                @Override
                public void run() {
                    Utils.sleep(1000);
                    stop = true;
                    System.out.println("updater set stop true.");
                    synchronized (lockObject) {
                    }
                }
            }, "updater");
    
            Thread getter = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        synchronized (lockObject) {
                        }
                        if (stop) {
                            System.out.println("getter stopped.");
                            break;
                        }
                    }
                }
            }, "getter");
            updater.start();
            getter.start();
        }
    }

4 总结

Happens-Before原则在Java并发编程中的重要性不言而喻,不理解Happens-Before难以写出线程安全又高效的多线程代码。相比于Java内存模型Happens-Before原则在可见性的描述上要简单得多,但是仍然很拗口并难以琢磨。本文通过通俗化的语言并结合众多实例代码向读者展示了Happens-Before原则,希望对各位读者在理解Happens-Before原则时能有所帮助。

有任何问题均在可在公众号对话框中回复进一步交流。


关于作者

王建新,转转架构部服务治理负责人,主要负责服务治理、RPC框架、分布式调用跟踪、监控系统等。爱技术、爱学习,欢迎联系交流。

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。

关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~