52

我在 java 中看到了一些示例,它们在代码块上进行同步以更改某些变量,而该变量最初被声明为 volatile .. 我在单例类的示例中看到了,他们将唯一实例声明为 volatile 并同步了该块初始化那个实例......我的问题是为什么我们在同步它时声明它是易失的,为什么我们需要同时做这两个?是不是其中一个对另一个就足够了?

public class SomeClass {
    volatile static Object uniqueInstance = null;

    public static Object getInstance() {
        if (uniqueInstance == null) {
            synchronized (someClass.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new SomeClass();
                }
            }
        }
        return uniqueInstance;
    }
}

提前致谢。

4

5 回答 5

24

在这种情况下,如果第一次检查在同步块内,则同步本身就足够了(但不是,如果变量不是易失性的,一个线程可能看不到另一个线程执行的更改)。仅 Volatile 是不够的,因为您需要以原子方式执行多个操作。但要小心!您在这里拥有的是所谓的双重检查锁定 - 一个常见的习惯用法,不幸的是它不能可靠地工作。我认为自 Java 1.6 以来这已经发生了变化,但这种代码仍然可能存在风险。

编辑:当变量是 volatile 时,此代码自 JDK 5(不是我之前写的 6)以来可以正常工作,但在 JDK 1.4 或更早版本下它不会按预期工作。

于 2012-03-12T10:39:45.957 回答
7

这使用了双重检查锁定,注意它if(uniqueInstance == null)不在同步部分内。

如果uniqueInstance不是易失性的,它可能会使用部分构造的对象“初始化”,其中除了在synchronized块中执行的线程之外,它的一部分不可见。在这种情况下,volatile 使其成为全有或全无的操作。

如果您没有同步块,则最终可能会有 2 个线程同时到达这一点。

if(uniqueInstance == null) {
      uniqueInstance = new someClass(); <---- here

并且您构造了 2 个 SomeClass 对象,这违背了目的。

严格来说,你不需要 volatile ,该方法本来可以

public static someClass getInstance() {
    synchronized(FullDictionary.class) {
         if(uniqueInstance == null) {
             uniqueInstance = new someClass();
          }
         return uniqueInstance;
    }
}

但这会导致执行 getInstance() 的每个线程的同步和序列化。

于 2012-03-12T10:44:27.253 回答
6

这篇文章解释了 volatile 背后的想法。

它还在开创性的著作Java Concurrency in Practice中得到了解决。

主要思想是并发不仅涉及对共享状态的保护,而且还涉及线程之间该状态的可见性:这就是 volatile 的用武之地。(这个更大的契约由Java 内存模型定义。)

于 2012-03-12T10:43:19.733 回答
0

您可以在不使用同步块的情况下进行同步。没有必要在其中使用 volatile 变量... volatile 更新主内存中的一个变量..并同步更新已从主内存访问的所有共享变量..因此您可以根据需要使用它..

于 2012-03-12T10:46:18.793 回答
-2

我的两分钱在这里

首先快速解释一下这段代码的直觉

if(uniqueInstance == null) {
        synchronized(someClass.class) {
            if(uniqueInstance == null) {
                uniqueInstance = new someClass();
            }
        }
    }

它检查 uniqueInstance == null 两次的原因是为了减少调用相对较慢的同步块的开销。所谓的双重检查锁定。

其次,它使用 synchronized 的原因很容易理解,它使 synchronized 块内部的两个操作具有原子性。

最后, volatile 修饰符确保所有线程都看到相同的副本,因此同步块之外的第一次检查将以与同步块“同步”的方式查看 uniqueInstance 的值。如果没有 volatile 修饰符,一个线程可以为 uniqueInstance 赋值,但另一个线程可能在第一次检查时看不到它。(虽然第二次检查会看到)

于 2015-05-14T07:08:55.377 回答