首页 > 解决方案 > 了解为什么根据 Java 内存模型在构造函数中启动线程是不安全的

问题描述

根据Java Concurrency in Practice,在类构造函数中启动线程是危险的。原因是这会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 打印到控制台。

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

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


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

标签: javamultithreadingjava-memory-model

解决方案


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

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.


推荐阅读