考虑下面不使用volatile的双重检查锁定的非传统实现:
public class ValueProvider {
private static State state = new Initial();
public static Value getValue() {
return state.getValue();
}
private static class Initial implements State {
@Override
public synchronized Value getValue() {
if (state instanceof Initial) {
Value value = new Value();
value.x = 1;
value.y = 2;
state = new Initialized(value);
return value;
} else {
return state.getValue();
}
}
}
private static class Initialized implements State {
private final Value value;
private Initialized(Value value) {
this.value = value;
}
@Override
public Value getValue() {
return value;
}
}
private interface State {
Value getValue();
}
public static final class Value {
private int x;
private int y;
public int getX() {
return x;
}
public int getY() {
return y;
}
}
}
这段代码是否是线程安全的?
具体来说,我问的是最终字段及其给出的保证,所以问题可能会被重新制定,因为某些线程可能获得一个非初始化的Value实例?
更新:删除了有关setter的提及,以便在发布后只能读取
答案 0 :(得分:0)
不,这不是线程安全的。在读取ValueProvider.state时没有内存障碍,在Value上根本没有。
Java兼容性的经验法则是,在写入时,读取和需要存储器障碍。
在Java中添加内存屏障的唯一方法是:
对于大多数事情,Hotspot会忽略final关键字,并且更喜欢自己推断它。然而,最终影响JMM的地方与类构造和内联有关。最后字段的重新排序规则包含在您已经提到的食谱中。它没有提到最后的课程。食谱说:
Loads and Stores of final fields act as "normal" accesses with respect to locks and volatiles, but impose two additional reordering
1)最终字段的存储(在构造函数内部,如果字段是引用,则此最终可以引用的任何商店,不能与后续存储重新排序
2)对于包含最终字段的对象的初始加载,无法对最终字段的初始加载(即线程的第一次遇到)进行重新排序。
答案 1 :(得分:0)
嗯,除了你的方法过于复杂Bohemian ♦ has pointed out之外,它还可以用于发布。如果两个线程同时访问getValue()
,则只有一个线程可以进入synchronized
块。由于synchronized
字段初始化保证,另一个将在Initialized
数据块上被阻止,或者看到value
的实例具有正确初始化的final
字段。
但是,它仍然无效,因为类Value
的实例是 mutable 而您的注释// getters and setters
表示实例将在构造后发生变异。在这种情况下,整个final
字段初始化保证是没有意义的,因为Value
类不是线程安全的。您可能会看到x
和y
的默认值,但您永远不会知道您将看到的有关后续修改的值以及(x
,{{1 }})不一定一致。