42

在阅读有关内存一致性错误的 Java 文档时。我发现与创建发生的两个动作相关的点 - 在关系之前:

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

  • 当一个线程终止并导致Thread.join()另一个线程中的 a 返回时,终止线程执行的所有语句 与成功连接之后的
    所有语句都具有发生前的关系。
    线程中代码的效果现在对执行连接的线程可见。

我无法理解它们的含义。如果有人用一个简单的例子来解释它会很棒。

4

4 回答 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.start并且Thread.join没有“发生在之前”的保证- 会给你这样的代码问题;

a = 1
Thread.start newthread
...

newthread:
    do_computation(a)

...因为a线程启动时可能没有将值存储到内存中。

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

它只是使线程更容易。

于 2013-04-27T06:29:27.647 回答
26

考虑一下:

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();

于 2013-04-27T06:38:17.963 回答
8

根据 java 内存模型未正确同步的代码中可能会出现线程可见性问题。由于编译器和硬件优化,一个线程的写入并不总是对另一个线程的读取可见。Java 内存模型是一种形式化的模型,它使“正确同步”的规则变得清晰,从而使程序员可以避免线程可见性问题。

Happens-before是该模型中定义的关系,它指的是特定的执行。假设没有其他干扰写入(即与读取没有发生之前发生关系的写入,或者在它们之间发生根据这种关系)。

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

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

这有什么实际意义?例如,如果您启动一个线程并等待它以非安全方式终止(例如长时间休眠,或测试一些非同步标志),那么当您尝试读取由线程,您可能会部分看到它们,因此存在数据不一致的风险。join() 方法充当屏障,保证线程发布的任何数据片段对另一个线程完全且一致地可见。

于 2013-04-27T06:31:17.227 回答
3

根据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 的主体中都是可见的。

并且行[Thread main] Finish count: 2表示 Thread 主体中的所有更改对调用 Thread.join() 的主线程都是可见的。

希望它可以帮助你清楚。

于 2014-09-05T08:07:33.647 回答