【问题标题】:Is the following java code thread safe without volatile?以下java代码线程安全没有volatile吗?
【发布时间】:2021-03-16 11:12:37
【问题描述】:
public static Singleton singleton;

public static Singleton get(){
    synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
    } 
    return singleton;
}

有人说singleton 变量没有 volatile 是错误的。但我认为这是创建单例对象的正确代码。我想知道这段代码是否有线程安全?

【问题讨论】:

  • 此代码已正确同步。所以它会正常工作;但是,如果您不想在每次获得价值时都为进入锁付出代价,那么还有更好的替代方案。这就是双重检查锁定问题的根源,然后您很快就会遇到易失性。

标签: java multithreading parallel-processing synchronization volatile


【解决方案1】:

正如anatolyg 指出的那样,您应该将字段singleton 设为私有,以避免对该字段进行不必要的非线程安全访问。

此外,即使在:

public static Singleton get(){
    synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
    } 
    return singleton;
} 

return singleton;synchronized 块之外,这段代码仍然是线程安全的,因为剩余的代码在 synchronized 块内,因此,该块内的所有线程将强制 happens-before 关系(如果实例设置正确,线程将无法返回 null)。

话虽如此,请注意:引用Holger

只要对单例的实际写入发生在开始之前 同步块,一切正常。 它只有效 因为最多只有一次写,肯定是在 返回。如果可以进行更多写入,它们可能会同时发生 到同步块之外的返回语句。

有一个完整的SO Thread 解决了为什么将return singleton 留在synchronized 块之外是线程安全的。

尽管如此,我与this one等其他用户的观点相同

由于返回不占用任何CPU或任何东西,没有理由 为什么它不应该在同步块内。如果是那时 如果我们在 Singleton 中,该方法可以标记为同步 在这里上课。如果单例获得,这将更清洁更好 在别处修改。

也就是说,您不需要 volatile 子句,因为您正在同步变量 singleton 的初始化。在这种情况下,synchronized 子句不仅保证多个线程不会访问:

    if (singleton == null) {  
        singleton = new Singleton();  
    }  

而且每个线程都会看到singleton 字段的最新引用。因此,不会发生多个线程将不同对象实例分配给singleton 字段的竞争条件

有人说单例变量没有 volatile 是错误的。

可能此人将您的代码误认为double-checked locking pattern,这是对您展示的版本的性能优化。在您的版本中,线程将在每次调用 get 方法时进行同步,这在变量 singleton 正确初始化后没有必要。这是双重检查锁定模式试图避免的开销。为了实现 volatile 是必需的(您可以阅读对此SO Thread 的深入解释),可以找到有关此双重检查锁定模式的更多信息here

【讨论】:

    【解决方案2】:

    几乎。如果您使用synchronized 块对变量/字段进行线程安全访问,则如果您在同一个锁下进行读写(在同一个对象的监视器上同步),则代码是正确的。因此,在您的代码中,您应该防止“private”修饰符而不是“public”声明读取正常(没有任何内存障碍)。

    private static Singleton singleton; // now we don't have direct access (to read/write) the field outside
    
    public static Singleton get(){
        synchronized (Singleton.class) { // all reads in the synchronized Happens-Before all writes
            if (singleton == null) { // first read
                singleton = new Singleton(); // write
            }
        } 
        return singleton; // the last normal read
    }
    

    您可能会注意到最后一次读取return singleton; 是正常的,但它是在第一次同步读取(如果单例不为空)或写入(如果为空)之后的后续(按程序顺序)读取,并且不需要放置在同步块内,因为 PO->HB 用于单个线程(https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5 "如果 x 和 y 是同一个线程的动作,并且 x 在程序顺序中位于 y 之前,则 hb(x, y)")。

    但从我的角度来看,以下结构似乎更惯用

    // all access is under the lock
    synchronized (Singleton.class) {  
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
     }
    

    或者只是

    public static synchronized Singleton get() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
    

    现在代码是正确的,但可能效率不高。这将我们带到了Double-Checked Locking 成语。您可以在 https://shipilev.net/blog/2014/safe-public-construction/

    顺便说一句,有时甚至以下代码也可以:

    private static volatile Singleton singleton;
    
    public static Singleton get() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
    

    此代码没有数据竞争,而是竞争条件。它总是返回一个 Singleton 的实例,但并不总是相同的。换句话说,在第一次调用 get() 时,我们可能会看到返回的 Singleton 的不同实例,它们相互重写到 singleton 字段,但如果你不在乎 :)...(小的不可变/例如具有相同状态的只读单例)

    【讨论】:

      【解决方案3】:

      您的代码中的变量不需要volatile。但是您的代码存在一些性能缺陷。每次get 中的代码将在synchronized 块内运行,这几乎不会导致性能开销。您可以使用双重检查锁定机制来避免性能开销。下面的代码展示了在 java 中使用双重检查锁定机制创建线程安全单例的正确方法。

        class Singleton {
          private static volatile Singleton singleton = null;
          public static Singleton get() {
              if (singleton == null) {
                  synchronized(this) {
                      if (singleton == null)
                          singleton = new Singleton();
                  }
              }
              return singleton;
          }
      }
      

      有关更多详细信息,请访问此link 并滚动到底部“使用 Volatile 修复双重检查锁定”。

      【讨论】:

      • 不清楚你为什么认为他们的代码会产生两个实例。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-12-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多