繁体   English   中英

是否建议始终对同步块/方法使用volatile变量?

[英]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个线程t1t2试图获取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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM