讨论这个答案时,我想知道为什么在分配默认值时不使用同步。
class StateHolder {
private int counter = 100;
private boolean isActive = false;
public synchronized void resetCounter() {
counter = 0;
isActive = true;
}
public synchronized void printStateWithLock() {
System.out.println("Counter : " + counter);
System.out.println("IsActive : " + isActive);
}
public void printStateWithNoLock() {
System.out.println("Counter : " + counter);
System.out.println("IsActive : " + isActive);
}
}
这个类看起来是线程安全的,因为对其字段的访问是由同步方法管理的。这样,我们所要做的就是安全地发布它。例如:
public final StateHolder stateHolder = new StateHolder();
它能被认为是一份安全的出版物吗?我认为不可能。查阅最后一个领域的语义(强调我的),我发现唯一值得注意的是stateHolder
引用不是一个过时的引用
一个只能在对象之后看到对该对象的引用的线程已完全初始化,则可以确保正确查看对象的最终字段的初始化值。
final
字段语义不关心final
字段引用的ojbect的状态。这样,另一个线程可能会看到字段的默认值。
问题:如何保证在构造函数或实例初始值设定项中分配的字段值的内存一致性?
我认为我们必须声明它们volatile
或final
,因为在分配引用和构造函数调用之间的关系之前不会发生任何事情。但是很多库类并没有以这种方式声明字段。java.lang.String就是一个例子:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence{
//...
private int hash; //neither final nor volatile
//...
}
final
可以保证您在实例构造后看到实例变量的赋值,而不需要任何进一步的操作。您只需要确保不会泄露构造函数中构造的实例。
volatile
还可以保证您将看到为某个实例变量设置的默认值,因为根据JLS 12.5"新建类实例",实例变量初始值设定项保证在构造函数结束之前执行。
安全发布并不是一件小事,但如果你坚持使用一种流行的机制来实现它,你应该会很好。您可以查看Java中的安全发布和安全初始化,以了解更多有趣的细节。
至于String.hash
,它是所谓的良性数据竞赛的一个流行例子。对hash
实例变量的访问允许一次读写的竞争和两次写的竞争。为了说明后者,两个线程可以同时:
- 查看0的初始值
- 决定他们是第一个计算散列的人
- 计算哈希代码并在不进行任何同步的情况下写入同一变量
由于两个原因,这场比赛仍然被允许并被认为是良性的:
- 针对不可变
String
实例的哈希码计算是一个幂等运算 - 32位值的写入保证不会撕裂
即使是良性的数据竞赛仍然不被推荐。参见Benign数据竞赛:什么可能出错?或者说,非决定论是不可避免的,但数据竞赛纯粹是邪恶的。