是的,私有成员变量bar
应该final
对吗?但实际上,在这种情况下,简单地读取int
的值是一个原子操作。这在技术上是线程安全吗?
class Foo {
private int bar;
public Foo(int bar) {
this.bar = bar;
}
public int getBar() {
return bar;
}
}
//假设无数个线程在getBar
的同一个实例上重复调用Foo
。
编辑:
假设这是Foo
类的所有代码;任何引用Foo
实例的线程都无法更改bar
的值(不会像使用反射等那么长)。
答案 0 :(得分:7)
最后更新:所以我的第一个结论是正确的,只是我的推理是错误的:-(我重新编辑了我的答案,使其有些连贯,不要隐藏我早先的痕迹的错误。
正如@Wyzard指出的那样,即使构建后无法更改bar
,Foo
仍然不是线程安全的。问题不是原子性,而是可见性。如果线程1在构造函数中更改bar
的值(从其默认值0开始),则无法保证其他线程何时可以看到新值(或者它们是否完全看到它 )。
所以foo
看起来像一个不可变对象。引自Java Concurrency in Practice,第3.4节:
如果出现以下情况,对象是不可变的:
- 施工后其状态无法修改;
- 所有领域都是最终的;和
- 构造正确(此参考在施工期间不会逃逸)。
Foo
在1)和3)看起来没问题,但不是2)。由于上面的推理,这是一个关键点。声明变量final
是确保其在不同线程之间可见性的一种方法。其他方法是声明bar
volatile
,或同步其访问方法。但是,当然,在不可变对象的情况下,这些都没有多大意义。
为什么final
字段可以保证可见性?来自Java Concurrency in Practice的部分,第3.5.2节:
因为不可变对象非常重要,所以JavaMemory Model提供了初始化安全的特殊保证,用于共享不可变对象。正如我们所见,对象引用变得对另一个线程可见并不一定意味着该对象的状态对于消费线程是可见的。为了保证对象状态的一致视图,需要同步。
另一方面,即使不使用同步来发布对象引用,也可以安全地访问不可变对象。为保证初始化安全性,必须满足所有对不变性的要求:不可修复的状态,所有字段都是最终的,以及正确的构造。 [...]
任何线程都可以安全地使用不可变对象而无需额外同步,即使不使用同步来发布它们也是如此。
此保证扩展到正确构造的对象的所有最终字段的值;无需额外同步即可安全访问最终字段。但是,如果final字段引用可变对象,则仍需要同步来访问它们引用的对象的状态。
如果该字段不最终会发生什么?其他线程可能会默默地看到该字段的陈旧值。没有例外或任何形式的警告 - 这就是为什么这些类型的错误难以追踪的原因之一。