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

时间:2015-12-08 06:59:27

标签: java multithreading concurrency

据我了解,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;
    }
}

假设我们有两个线程t1t2试图获取Singleton对象。线程t1首先输入getInstance()方法并创建INSTANCE对象。现在,这个新创建的对象应该对所有其他线程可见。如果INSTANCE变量不是volatile,那么我们如何确保该对象仍然不在t1's内存中并且对其他线程可见。上述INSTANCEt1初始化到其他线程的时间有多快?

这是否意味着始终使变量易变为同步? 在什么情况下我们不要求变量是易变的?

P.S我已经阅读了有关StackOverflow的其他问题,但无法找到我的问题的答案。请在投票前发表评论。

我的问题来自here

给出的解释

2 个答案:

答案 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字段,那么它应该具有相同的结果,但需要稍长一些。