Java的Happen-Before原则

Java的Happen-Before原则是一组控制如何允许Java虚拟机和CPU重新排序指令以提高性能的规则。这些规则保证了在规则中先执行的操作对后执行的操作有可见性。

指令重排

在理解这个复杂概念前,先来看看指令重排。现代CPU都能在指令不互相依赖的情况下并行执行指令,具体的例子如下例1,①和②两天指令是可以在CPU上并行运行的,因为它们之间并没有依赖关系。

// 例1
① a = b + c
② d = e + f
复制代码

但假如情况如下例2,指令②就必须指令①执行完毕后执行,因为它们之间存在依赖关系。

// 例2
① a = b + c
② d = a + f
复制代码

指令重排的意义在于增加CPU中指令的并行执行,从而提升CPU性能。

Happen-Before原则

在例2中,②必须在①之后执行,因为要保证①指令的a值更新要对②指令可见,那么如果这两个指令在由不同的线程执行呢?换句话说,多线程下如何保证一个线程的操作对另一个线程是可见的呢?

可见性

如果一个线程的操作对其他线程有可见性,那么这个线程操作产生的结果也会被其他线程感知。

我们都知道现代计算机有多核CPU,每个CPU上都有相应的cache,对主存中共享变量的写入操作的可见性可能会由于每个内核中的缓存而在写入主存时出现延迟,从而导致问题。这可能导致另一个线程读取该变量的旧值(而不是最后更新的值)。

来看一个多线程读写共享内存而出错的例子。

public class StopThread {

    private static boolean stopRequested;
    
    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    System.out.println(i++);
                }
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}
复制代码

backgroundThread将打印增加i的值,直到stopRequested变为true。通过主线程启动线程后,它将休眠1秒钟,并使stopRequested变为true。

那么理论上,程序会打印一秒钟的i的自增过程,然后backgroundThread将因为stopRequested变为true而跳出循环而结束。

但是,如果在多核计算机上运行程序,结果是无限打印i的自增过程。 在stopRequested执行写操作时会出现问题,无法保证stopRequested的更改对backgroundThread可见,因此它将进入无限循环。

当主线程和backgroundThread在不同内核上运行时,stopRequested将被加载到执行backgroundThread的内核的高速cache中。主线程会将stopRequested的更新值保留在其他内核的高速cache中。由于现在stopRequested值位于两个不同的cache中,因此backgroundThread可能感知不到更新后的值。

Happens-before relationship

可以通过Happens-before relationship来排序两个动作。如果一个动作发生在另一个动作之前,则第一个动作对第二个动作可见,并在第二个动作之前排序。

如何建立Happens-before relationship呢?可以参考以下的规则:

  • Single thread rule:单线程中的每个动作都发生在该线程中的每个动作之前,该顺序在程序顺序中排在后面。
  • Monitor lock rule:synchronized关键字
  • Volatile variable rule:volatile关键字
  • Thread start rule:在启动线程之前,都会调用Thread.start()。假设线程A通过调用threadA.start()产生了一个新线程B,线程B对线程A可见。
  • Thread join rule:假设线程A通过调用threadA.start()然后调用threadA.join()来生成新线程B。线程A将在join()调用之前等待,直到线程B的run方法完成为止。join方法返回后,线程A中的所有后续操作都发生在线程B的run方法中执行的所有操作之后。
  • Transitivity:如果A happens-before B, 并且B happens-before C, 那么A happens-before C。

如果在写操作和读操作之间存在Happens-before relationship,则可以保证一个线程的写结果对于另一线程的读取是可见的。上述的可见性例子可以通过synchronized和volatile关键字来修复问题。

Synchronizing

public class StopThread {

    private static boolean stopRequested;
    
    private static synchronized void requestStop() {
        stopRequested = true;
    }
    
    private static synchronized boolean stopRequested() {
        return stopRequested;
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested())
                    System.out.println(i++);
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}
复制代码

Volatile Fields

public class StopThread {

    private static volatile boolean stopRequested;
    
    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested())
                    System.out.println(i++);
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}
复制代码

当然,volatile关键字并不能完全替代synchronized,它只能保证变量的可见性,并不能保证操作的原子性,只是在解决这个问题上有相同的效果。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享