讨论这个答案我想知道为什么我们在分配默认值时不使用同步。
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
字段引用的对象的状态。这样,另一个线程也可能会看到字段的默认值。
问题:我们如何保证构造函数或实例初始化程序中分配的字段值的内存一致性?
我认为我们必须将它们声明为
volatile
或 final
,因为在分配引用和构造函数调用之间不存在 happens-before 关系。但许多库类并不以这种方式声明字段。 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
在 MacO 上 得到了
Results across all configurations:
RESULT SAMPLES FREQ EXPECT DESCRIPTION
-2 5,941,670,743 79.63% Acceptable Not initialized yet
42 1,519,843,765 20.37% Acceptable Boring