具有非final字段的不可变对象如何是线程不安全的?

Nim*_*sky 8 java concurrency multithreading java-memory-model

说我们有这个

// This is trivially immutable.
public class Foo {
    private String bar;
    public Foo(String bar) {
        this.bar = bar;
    }
    public String getBar() {
        return bar;
    }
}
Run Code Online (Sandbox Code Playgroud)

是什么让这个线程不安全?继这个问题之后.

ass*_*ias 11

Foo一旦安全发布,它就是线程安全的.例如,这个程序可以打印"不安全"(它可能不会使用hotspot/x86的组合) - 如果你做出bar最终结果就不会发生:

public class UnsafePublication {

    static Foo foo;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (foo == null) {}
                if (!"abc".equals(foo.getBar())) System.out.println("unsafe");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                foo = new Foo("abc");
            }
        }).start();
    }
}
Run Code Online (Sandbox Code Playgroud)


Phi*_*ipp 7

由于JVM优化,您永远不能假设操作按照它们的编写顺序执行,除非它对同一个线程很重要.因此,当您调用构造函数然后将对结果对象的引用传递给另一个线程时,JVM可能实际上不会在同一个线程中需要之前写入foo.bar的值.

这意味着在多线程环境中,可以在构造函数中的值写入之前调用getBar方法.


Eug*_*ene 5

到目前为止,您很可能已经得到了答案,但是只是为了确保我也想添加我的解释。

为了使对象(就您的情况而言)是线程安全的,它必须:

  • 一成不变
  • 安全发布

不可变 -您做到了。一旦设置了酒吧,就无法修改它。这里很明显。

安全发布。根据示例,该代码未安全发布。因为bar不是最终的,编译器可以根据需要自由对其进行重新排序。在写入bar 之前,编译器可以发布(写入主内存)对Foo实例的引用。那将意味着bar为空。因此,首先将对Foo的引用写入主内存,然后发生对bar的写入。在这两个事件之间,另一个线程可以将陈旧的栏视为空。

如果将final添加到它,则JMM将保证:

保证final字段的值对访问构造对象的其他线程可见。

或者,最后一个字段可防止重新排序。因此,使该变量为final将确保线程安全。