Java中的内存一致性- happens-before关系

42

在阅读Java文档中的关于内存一致性错误的内容时,我发现了两个相关的点,它们可以创建 happen-before 关系:

  • 当一个语句调用 Thread.start() 时,与该语句具有 happens-before 关系的每个语句也都具有与新线程执行的每个语句都具有 happens-before 关系。导致创建新线程的代码的效果对新线程可见。

  • 当一个线程终止并导致在另一个线程中返回 Thread.join() 时,终止的线程执行的所有语句都与成功 join 后执行的所有语句具有 happens-before 关系。现在,线程中代码的影响对执行 join 操作的线程可见。

我无法理解它们的含义。如果有人能用简单的例子来解释一下就太好了。


7
“happens-before relationship” 的意思是一组语句在另一组语句执行之前得到保证会被执行。因此,在第一个场景中,导致新线程启动的语句与新线程将要执行的语句具有 happens-before 关系。这些语句所做的任何更改都将对该线程执行的语句可见。 - Dev Blanked
3
我发现这个页面很有帮助:http://preshing.com/20130702/the-happens-before-relation/ 它提供了关于A和B之间“先于发生”关系与A实际上在B之前发生的区别的例子。 - Josiah Yoder
4个回答

42

现代的CPU并不总是按照数据更新的顺序将数据写入内存,例如,如果你运行这段伪代码(为简单起见,假设变量始终存储在内存中);

a = 1
b = a + 1

在单个线程中,CPU 写入 b 到内存之前可能会写入 a 到内存,这并不是真正的问题,因为一旦进行赋值操作,运行上述代码的线程将不会再看到变量的旧值。

多线程则另当别论。你可能认为以下代码可以让另一个线程获取您的大量计算的值:

a = heavy_computation()
b = DONE

...另一个线程正在执行...

repeat while b != DONE
    nothing

result = a

问题在于,在结果被存储到内存之前,完成标志可能已经被设置在内存中,因此其他线程可能会在计算结果被写入内存之前获取内存地址a的值。

如果Thread.startThread.join没有“happens before”保证,同样的问题也会出现在如下代码中:

a = 1
Thread.start newthread
...

newthread:
    do_computation(a)

由于线程启动时可能未将值存储到内存中,因此需要注意。

由于您几乎总是希望新线程能够使用在启动前初始化的数据,因此 Thread.start 具有“happens before”保证, 也就是说,在调用Thread.start 之前更新的数据保证对新线程可用。同样,Thread.join 也是如此,其中新线程写入的数据保证在终止后对加入它的线程可见。

这使得线程处理更加容易。


29

考虑以下内容:

static int x = 0;

public static void main(String[] args) {
    x = 1;
    Thread t = new Thread() {
        public void run() {
            int y = x;
        };
    };
    t.start();
}

主线程已更改字段x。Java内存模型不能保证如果其他线程未与主线程同步,则此更改将对其可见。但线程t将看到此更改,因为主线程调用了t.start(),而JLS保证调用t.start()会使x的更改在t.run()中可见,因此y被保证分配为1
同样适用于Thread.join();

好的,我同意,稍微修改了答案以避免批评,请现在看一下。 - Evgeniy Dorofeev
线程t具有与主线程不同的执行路径。由于没有在同一对象上进行同步,因此没有保证。线程t的执行确实是由主线程启动的。但仅仅因为主线程c/d了内部线程,并不能保证可见性的变化。您能否给出JLS中关于这一点的参考 - JLS保证调用t.start()会使x的更改在t.run()中可见。 - Farhan stands with Palestine
我在JLS中找到了这一点。它也在JCIP第16章中提到。Java内存模型-->> happens-before规则是:线程启动规则。对线程调用Thread.start发生在启动线程中的每个操作之前。你是正确的。我已经为它点赞了。 - Farhan stands with Palestine

8

线程可见性问题可能会在代码中发生,如果该代码未按照Java内存模型进行正确同步。由于编译器和硬件优化,一个线程的写入并不总是能被另一个线程的读取所看到。Java内存模型是一个正式模型,它明确了“正确同步”的规则,以便程序员可以避免线程可见性问题。

happens-before是该模型中定义的一种关系,它涉及到特定的执行。一个已被证明happens-before一个读取R的写入W保证了该读取可以看到该写入,假设没有其他干扰性写入(即与该读取没有happens-before关系或根据该关系在它们之间发生的写入)。

最简单的happens-before关系类型发生在同一线程中的操作之间。线程P中对V的写入W happens-before 同一线程中V的读取R,假设W根据程序顺序在R之前。

您所参考的文本指出thread.start()和thread.join()也保证了happens-before关系。任何发生在thread.start()之前的操作也发生在该线程内的任何操作之前。同样,线程内的操作发生在thread.join()之后出现的任何操作之前。

这有什么实际意义呢?例如,如果您以非安全方式启动一个线程并等待它终止(例如长时间睡眠或测试某些非同步标志),那么当您尝试读取线程所做的数据修改时,您可能会看到它们部分地,从而存在数据不一致的风险。join()方法充当了一个屏障,保证了线程发布的任何数据都可以被其他线程完整和一致地看到。


4
根据Oracle文档,他们定义了“happens-before关系”,只是保证一个特定语句的内存写入被另一个特定语句所可见
package happen.before;

public class HappenBeforeRelationship {


    private static int counter = 0;

    private static void threadPrintMessage(String msg){
        System.out.printf("[Thread %s] %s\n", Thread.currentThread().getName(), msg);
    }

    public static void main(String[] args) {

        threadPrintMessage("Increase counter: " + ++counter);
        Thread t = new Thread(new CounterRunnable());
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            threadPrintMessage("Counter is interrupted");
        }
        threadPrintMessage("Finish count: " + counter);
    }

    private static class CounterRunnable implements Runnable {

        @Override
        public void run() {
            threadPrintMessage("start count: " + counter);
            counter++;
            threadPrintMessage("stop count: " + counter);
        }

    }
}

输出将会是:
[Thread main] Increase counter: 1
[Thread Thread-0] start count: 1
[Thread Thread-0] stop count: 2
[Thread main] Finish count: 2

请看输出,第 [Thread Thread-0] start count: 1 行显示在调用 Thread.start() 方法之前的所有计数器变化都可见于该线程的主体中。
而第 [Thread main] Finish count: 2 行表示 Thread 主体中的所有变化都对调用 Thread.join() 的主线程可见。
希望这可以帮助您更清楚地理解。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接