讨论this answer我想知道为什么我们在分配默认值时不使用同步。
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();
它可以被视为安全出版物吗?我认为不,不能。咨询final field semantic (强调我的)我发现唯一可以保证的是 stateHolder
引用不是过时的。 :
A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.
final
字段语义不关心 final
字段引用的对象的状态。这样,另一个线程也可能会看到字段的默认值。
问题:我们如何保证构造函数或实例初始值设定项中分配的字段值的内存一致性?
我认为我们必须将它们声明为 volatile
或最终
,因为没有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 Creation of New Class Instances 构造函数结束之前执行。
安全发布并非完全微不足道,但如果您坚持使用一种流行的机制来实现它,那么您应该完全没问题。您可以查看 Safe Publication and Safe Initialization in Java 了解一些更有趣的细节。
至于String.hash
,它是所谓的良性数据竞争的一个流行示例。对 hash
实例变量的访问允许读取和写入竞争以及两次写入竞争。为了说明后者,两个线程可以同时:
- 查看初始值0
- 决定他们是第一个计算哈希值的人
- 计算哈希码并写入同一变量,无需任何同步
由于两个原因,比赛仍然被允许并被认为是良性的:
- 不可变
String
实例的哈希码计算是幂等操作。 - 保证 32 位值的写入不会损坏。
不过,即使是良性的数据争用仍然不推荐。请参阅 Benign data races: what could possibly go wrong? 或 Nondeterminism is unavoidable, but data races are pure evil 。
关于java - 字段的默认值是否保证在线程中可见?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36445294/