[英]Is it advisable to always use volatile variables with synchronized blocks/methods?
据我了解, 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;
}
}
假设我们有2个线程t1
和t2
试图获取Singleton
对象。 线程t1
进入getInstance()
方法并创建INSTANCE
对象。 现在,这个新创建的对象应该对所有其他线程可见。 如果INSTANCE
变量不是volatile
那么我们如何确保该对象仍然不在t1's
内存中并且对其他线程可见。 由t1
初始化的上述INSTANCE
多久可见其他线程?
这是否意味着始终使变量易变为同步? 在什么情况下我们不要求变量是易变的?
PS我已经在StackOverflow上阅读了其他问题,但找不到我的问题的答案。 请在投票前发表评论。
我的问题来自这里给出的解释
我认为你所缺少的是JLS 17.4.4 :
监视器m上的解锁动作与m上的所有后续锁定动作同步(其中“后续”根据同步顺序定义)。
这与关于volatile变量的内容非常相似:
对易失性变量v(第8.3.1.4节)的写入与任何线程对v的所有后续读取同步(其中“后续”根据同步顺序定义)。
然后在17.4.5:
如果动作x与后续动作y同步,那么我们也有hb(x,y)。
......其中hb是“发生在之前”的关系。
然后:
如果一个动作发生在另一个动作之前,那么第一个动作在第二个动作之前可见并且在第
内存模型非常复杂,我并不认为是专家,但我的理解是引用部分的含义是你所展示的第二种模式是安全的,而变量不是易变的 - 实际上任何变量都是仅在同步块内修改和读取同一个监视器是安全的,不会出现波动。 对我来说,更有趣的方面是变量值引用的对象内的变量会发生什么。 如果Singleton
不是不可变的,那么你仍然可能会遇到问题 - 但是这一步就被删除了。
更具体地说,如果两个线程在INSTANCE
为空时调用getInstance()
,则其中一个线程将首先锁定监视器。 在解锁操作之前发生对INSTANCE
的非空引用的写入操作,并且在另一个线程的锁定操作之前发生解锁操作。 锁定操作发生在读取INSTANCE
变量之前,因此写入发生在读取之前...此时,我们保证写入对读取线程可见。
这里对正在发生的事情的解释是完全错误的,因为我误解了Java内存模型。 请参阅Jon Skeet的回答 。
在这种情况下,您尝试的操作是“延迟初始化”,并且该特定模式对于实例非常有用,但对于静态变量则是次优的。 对于静态变量,首选延迟初始化持有者类习惯用法 。
以下引用和代码块直接从Josh Bloch的Effective Java(第2版)第71项中复制:
因为如果字段已经初始化,则没有锁定,因此将字段声明为volatile是至关重要的。
// 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)在synchronized
块内部 (再次)检查它。 外部检查有助于提高效率; 内部检查保证单个初始化。
我强烈建议您阅读Effective Java(第2版)的第71项,以获得更完整的解释。 我也推荐这本书一般很棒。
更新:
使用的本地result
变量减少了所需的volatile
字段的访问次数,从而提高了性能。 如果遗漏了局部变量,并且所有读写都直接访问了volatile
字段,那么它应该具有相同的结果,但需要稍长一些。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.