为啥将 volatile 与同步块一起使用?

Posted

技术标签:

【中文标题】为啥将 volatile 与同步块一起使用?【英文标题】:why using volatile with synchronized block?为什么将 volatile 与同步块一起使用? 【发布时间】:2012-03-28 18:12:51 【问题描述】:

我在 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;
    

提前致谢。

【问题讨论】:

什么是“volatile static uniqueInstance = null;” ? 【参考方案1】:

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

编辑:当变量为 volatile 时,此代码从 JDK 5(不是我之前写的 6)开始可以正常工作,但在 JDK 1.4 或更早版本下将无法正常工作。

【讨论】:

你说如果第一次检查在同步块内同步就足够了....但是我在进入同步块后再次进行相同的检查,所以下一个线程肯定会看到更新的变量的值。 提醒未来的读者:上面链接的文章已经过时了,正如编辑所述,该技术在大约 10 年前发布的 JDK 5 上运行良好。跨度> @MohammadDorgham @MichałKosmulski 第二个空检查总是会看到更新的值。那么这是否意味着volatile 仅用于使第一次空值检查失败以提高效率? (没有volatile 会很慢但仍然正确?) 来自 Oracle 文档“当一个同步方法退出时,它会自动与同一对象的同步方法的任何后续调用建立起之前的关系。这保证了对对象状态的更改是对所有线程可见”docs.oracle.com/javase/tutorial/essential/concurrency/…。我认为同步块也是如此。如果为 true,那么被阻塞的线程应该会看到“uniqueInstance”的更新值。 @WeishiZeng 是的,只是优化【参考方案2】:

这使用了双重检查锁定,注意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() 的每个线程的同步和序列化。

【讨论】:

我在if语句之后进行了同步以降低同步成本,所以我的问题是为什么我需要在进入同步块后仔细检查时让它变得不稳定?我的意思是同步会立即更新所有共享变量,所以下一个线程会看到 uniqueInstance 的更新值 @user1262445 正如我所说,没有 volatile 线程可以将对象视为部分构造的。所以它永远不会进入同步块,但如果 uniqueInstance 不是 volatile,则返回一个部分构造的对象。 IE。你有 1 个线程没有进入同步块,它可能会返回垃圾,因为另一个线程就在同步块的中间。 我的问题是没有volititle,确实第一个if(uniqueInstance == null) 虽然初始化了也可以通过,但是进入同步块后,由于已经同步,所以会修复miss判断所以 if(uniqueInstance == null) 将是 false 并且不会创建新实例,不是吗?因此,即使没有 valitle ,代码仍将按预期工作(仅创建一个相同的实例并返回)。但确实有较差的表现?我说的对吗? @nos 我和@Jaskey 有同样的疑问。 " 只有在构造函数返回时,才会将新对象分配给变量。uniqueInstance 的默认值是null。所以在synchronized 块内,它怎么可能是一个部分初始化的对象被分配给uniqueInstance @nos 谢谢你的回复!!是的,这已经接近我的问题了。对于第 2 步和第 3 步的顺序,您知道它记录在哪里吗? uniqueInstance = new someClass(); 在我看来,该对象仅在构造函数返回时才分配给uniqueInstance。 (正如我对这个 SO ***.com/questions/7187842/… 的理解)【参考方案3】:

This post 解释了 volatile 背后的想法。

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

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

【讨论】:

【参考方案4】:

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

【讨论】:

【参考方案5】:

我的两分钱在这里

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

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

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

第二,它使用synchronized的原因很容易理解,它使synchronized块内部的两个操作原子化。

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

【讨论】:

以上是关于为啥将 volatile 与同步块一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

2.3.7synchronized代码块有volatile同步的功能

注意,不能错过的CAS+volatile实现同步代码块

注意,不能错过的CAS+volatile实现同步代码块

注意,不能错过的CAS+volatile实现同步代码块

与 Java 中的 volatile 字段和同步块的关系发生在之前 - 以及它们对非易失性变量的影响?

正确使用 Volatile 变量