具有非final字段的不可变对象如何是线程不安全的?
说我们有这个
// This is trivially immutable. public class Foo { private String bar; public Foo(String bar) { this.bar = bar; } public String getBar() { return bar; } }
是什么让这个线程不安全? 继这个问题之后 。
一旦安全发布, 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(); } }
由于JVM优化,您永远不能假设操作按照它们的编写顺序执行,除非它对同一个线程很重要。 因此,当您调用构造函数然后将对结果对象的引用传递给另一个线程时,JVM可能实际上不会在同一个线程中需要之前写入foo.bar的值。
这意味着在multithreading环境中,可以在构造函数中的值写入之前调用getBar方法。
很可能你现在已经得到了答案,但只是为了确保我也想添加我的解释。
为了使对象(对于您的情况)是线程安全的,它必须:
- 是不可改变的
- 安全发布
永恒 – 你做到了。 设置后无法修改栏。 这里很明显。
安全发布 。 根据示例,代码未安全发布。 因为bar不是最终的,所以编译器可以根据需要自由重新排序。 在写入bar 之前 ,编译器可以发布(写入主存储器)对Foo实例的引用。 这意味着bar为null。 因此, 首先将对Foo的引用写入主存储器, 然后发生写入条形码。 在这两个事件之间,另一个线程可以将过时条视为null。
如果你添加final,JMM将保证:
保证最终字段的值对访问构造对象的其他线程可见。
或者,最终字段可防止重新排序。 因此,使变量最终将确保线程安全。
从评论中发布的链接 :
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 = fx; // guaranteed to see 3 int j = fy; // could see 0 } } }
一个线程可以调用writer()
,另一个线程可以调用reader()
。 reader()中的if条件可以求值为true,但是因为y不是最终的, 对象初始化可能还没有完全完成 (所以对象尚未安全发布),因此int j = 0
可能会发生,因为它没有已初始化。