据我了解,volatile
有助于内存可见性,synchronized
有助于实现执行控制。 Volatile
只保证线程读取的值将写入最新值。
请考虑以下事项:
public class Singleton{
private static volatile Singleton INSTANCE = null;
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE==null){
synchronized(Integer.class){
if(INSTANCE==null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
在上面的代码中,我们使用双重检查锁定。这有助于我们只创建一个Singleton实例,并且尽快通过创建线程将communicated
创建到其他线程。这就是关键字volatile
的作用。我们需要上面的synchronized block
,因为线程将INSTANCE
变量读取为null并初始化对象的延迟可能会导致race condition
。
现在考虑以下事项:
public class Singleton{
private static Singleton INSTANCE = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(INSTANCE==null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
假设我们有两个线程t1
和t2
试图获取Singleton
对象。线程t1
首先输入getInstance()
方法并创建INSTANCE
对象。现在,这个新创建的对象应该对所有其他线程可见。如果INSTANCE
变量不是volatile
,那么我们如何确保该对象仍然不在t1's
内存中并且对其他线程可见。上述INSTANCE
由t1
初始化到其他线程的时间有多快?
这是否意味着始终使变量易变为同步? 在什么情况下我们不要求变量是易变的?
P.S我已经阅读了有关StackOverflow的其他问题,但无法找到我的问题的答案。请在投票前发表评论。
我的问题来自here
给出的解释答案 0 :(得分:7)
我认为你遗失的是JLS 17.4.4:
监视器m上的解锁操作与m上的所有后续锁定操作同步(其中“后续”根据同步顺序定义)。
这与关于volatile变量的内容非常相似:
对易失性变量v(第8.3.1.4节)的写入与任何线程对v的所有后续读取同步(其中“后续”根据同步顺序定义)。
然后在17.4.5:
如果动作x与后续动作y同步,那么我们也有hb(x,y)。
...其中hb是“发生在之前”的关系。
然后:
如果一个动作发生在另一个动作之前,则第一个动作在第二个动作之前可见并且在第二个动作之前被命令。
内存模型非常复杂,我并不认为自己是专家,但我的理解是引用部分的含义是你所展示的第二种模式是安全的,而变量不会变化 - 实际上任何仅修改并在同一个监视器的同步块中读取的变量是安全的,不会出现波动。对我来说,更有趣的方面是变量值引用的对象内的变量会发生什么。如果Singleton
不是不可变的,那么你仍然可能会遇到问题 - 但这只是一步之遥。
更具体地说,如果两个线程在getInstance()
为空时调用INSTANCE
,则其中一个线程将首先锁定监视器。在解锁操作之前发生对INSTANCE
的非空引用的写操作,并且在另一个线程的锁定操作之前发生解锁操作。锁定操作发生在读取INSTANCE
变量之前,因此写入发生在读取之前...此时,我们保证写入对读取线程可见。
答案 1 :(得分:-2)
这里对正在发生的事情的解释是完全错误的,因为我误解了Java内存模型。请参阅Jon Skeet's answer。
在这种情况下,您尝试的操作是"延迟初始化",并且该特定模式对于实例有用,但对于静态变量是次优的。对于静态变量, lazy initialization holder class idiom 是首选。
以下引文和代码块直接由Josh Bloch的 Effective Java(第2版)的第71项复制:
因为如果字段已经初始化,则没有锁定 这个领域被宣布为不稳定是至关重要的。
// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField() {
FieldType result = field;
if (result == null) { // First check (no locking)
synchronized(this) {
result = field;
if (result == null) // Second check (with locking)
field = result = computeFieldValue();
}
}
return result;
}
在他的一次演讲中,他建议在为实例字段执行延迟初始化时复制此结构 ,因为它在这种情况下是最佳的,并且通过更改它很容易打破它
编辑:此部分不正确。
volatile
关键字表示变量的所有读写操作都是原子的;也就是说,从其他任何事物的角度来看,它们只是一步到位。此外,volatile
变量始终从主内存中读取和写入,不处理器缓存。这两个属性的组合保证了,只要在一个线程上修改了volatile变量变量,另一个线程上的后续读取就会读取更新的值。对于非volatile
变量,此保证不。
双重检查成语并不保证只创建一个实例。相反,它是这样的,一旦变量被初始化,未来对getInstance()
的调用不需要输入synchronized
块,这是昂贵的。
保证它没有被初始化两次是因为(a)它是一个易失性字段,(b)它被{{1}内部的检查阻止。外部检查有助于提高效率;内部检查保证单个初始化。
我强烈建议您阅读 Effective Java(第2版)的第71项,以获得更完整的解释。我也推荐这本书一般很棒。
更新:
使用的本地synchronized
变量减少了所需result
字段的访问次数,从而提高了性能。如果遗漏了局部变量,并且所有读取和写入都直接访问volatile
字段,那么它应该具有相同的结果,但需要稍长一些。