8

说我们有这个

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

是什么让这个线程不安全?从这个问题开始。

4

4 回答 4

12

Foo一旦安全发布,它就是线程安全的。例如,这个程序可以打印“不安全”(它可能不会使用热点/x86 的组合)——如果你做barfinal 它就不会发生:

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();
    }
}
于 2013-04-17T14:13:58.577 回答
7

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

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

于 2013-04-17T14:13:37.190 回答
6

很可能你现在已经得到了答案,但只是为了确保我也想添加我的解释。

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

  • 不可变
  • 安全发布

不可变- 你做到了。bar一旦设置就无法修改。这里很明显。

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

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

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

或者,final 字段阻止重新排序。因此,使该变量为 final 将确保线程安全。

于 2013-04-24T08:49:25.807 回答
1

从评论中发布的链接

class FinalFieldExample { 
    final int x;
    int y; 
    static FinalFieldExample f;

    public FinalFieldExample() {
        x = 3; 
        y = 4; 
    } 

    static void writer() {
        f = new FinalFieldExample();
    } 

    static void reader() {
        if (f != null) {
            int i = f.x;  // guaranteed to see 3  
            int j = f.y;  // could see 0
        } 
    } 
}

一个线程可能会调用writer(),而另一个线程可能会调用reader(). reader() 中的 if 条件可以评估为 true,但由于 y 不是 final,因此对象初始化可能尚未完全完成(因此对象尚未安全发布),因此int j = 0可能在尚未初始化时发生。

于 2013-04-17T14:35:01.607 回答