java - 从 Java 内存模型的角度理解为什么在构造函数中启动线程是不安全的

标签 java multithreading java-memory-model

<分区>

根据 Java Concurrency in Practice,在类构造函数中启动线程是危险的。原因是在对象完全构造之前,this 将 this 指针暴露给另一个线程。

尽管这个话题在之前的许多 StackOverflow 问题中都有讨论,但我仍然难以理解为什么这是一个如此令人担忧的问题。特别是,我希望从 Java 内存模型的角度澄清在构造函数内启动线程是否会导致内存一致性问题。

让我举一个具体的例子来说明我想做的事情。 (这段代码的期望输出是将数字 20 打印到控制台。)

private static class ValueHolder {

    private int value;
    private Thread thread;

    ValueHolder() {
        this.value = 10;
        thread = new Thread(new DoublingTask(this)); // exposing "this" pointer!!!
        thread.start();   // starting thread inside constructor!!!
    }

    int getValue() {
        return value;
    }

    void awaitTermination() {
        try {
            thread.join();
        } catch (InterruptedException ex) {}
    }

}

private static class DoublingTask implements Runnable {

    private ValueHolder valueHolder;

    DoublingTask(ValueHolder valueHolder) {
        this.valueHolder = valueHolder;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName());
        System.out.println(valueHolder.getValue() * 2);  // I expect to print out 20...
    }

}

public static void main(String[] args) {
    ValueHolder myValueHolder = new ValueHolder();
    myValueHolder.awaitTermination();
}

是的,我知道线程是在我们从构造函数返回之前启动的。是的,我知道 this 指针暴露给了线程。 然而,我确信代码是正确的。我相信它会始终将数字 20 打印到控制台。

  • 赋值 this.value = 10 happens-before thread.start()。 (这是因为 this.value = 10 在程序顺序中位于 thread.start() 之前。)
  • 主线程中 thread.start() 的调用发生在 .run() 方法开始之前新创建的线程。 (因为 thread.start() 是一个同步 Action 。)
  • .run() 方法的开始发生在 System.out.println(valueHolder.getValue() * 2);打印语句。 (同样,按节目顺序。)

因此,根据 Java 内存模型,打印语句应该读取 valueHolder.value 的正确初始化值(即 10)。因此,尽管忽略了Java Concurrency in Practice 的建议,我似乎仍然编写了一段正确的代码。

我做错了吗?我错过了什么?


更新:根据回答和评论,我现在明白我的代码示例在功能上是正确的,因为我提供的原因。但是,以这种方式编写代码是一种不好的做法,因为其他开发人员将来有可能在线程启动后添加更多的初始化语句。可能出现此类错误的一种情况是在实现此类的子类时。

最佳答案

假设我将你的类子类化。它可能在需要时尚未初始化其字段。

class BetterValueHolder extends ValueHolder
{
    private int betterValue;

    BetterValueHolder(final int betterValue)
    {
        // not actually required, it's added implicitly anyway.
        // just to demonstrate where your constructor is called from
        super();

        try
        {
            Thread.sleep(1000); // just to demonstrate the race condition more clearly
        }
        catch (InterruptedException e) {}

        this.betterValue = betterValue;
    }

    @Override
    public int getValue()
    {
        return betterValue;
    }
}

这将打印零,无论为 BetterValueHolder 的构造函数提供什么值。

关于java - 从 Java 内存模型的角度理解为什么在构造函数中启动线程是不安全的,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56132392/

相关文章:

java - 如何在多线程环境中交换 boolean 值?

java - 按顺序显示 x 个 ImageView y 秒

java - 监视特定方法中的 Java 对象分配/释放

java - 初始化非最终字段

java - 为什么Java中返回值是内存地址而不是实际值?

Java,默认编码

java - 如果用户线程已经完成工作,是否有任何技术可以强制守护线程完成某些任务?

Java 内存模型操作

java - 检查这是什么java类类型

java - Camel 中的 JPA 组件无法自动重新连接到数据库