【问题标题】:Publishing and reading of non-volatile field非易失性领域的发布和阅读
【发布时间】:2017-12-04 17:04:24
【问题描述】:
public class Factory {
    private Singleton instance;
    public Singleton getInstance() {
        Singleton res = instance;
        if (res == null) {
            synchronized (this) {
                res = instance;
                if (res == null) {
                    res = new Singleton();
                    instance = res;
                }
            }
        }
        return res;
    }
}

这几乎是线程安全Singleton 的正确实现。我看到的唯一问题是:

初始化instance 字段的thread #1 可以在完全初始化之前发布。现在,第二个线程可以在不一致的状态下读取instance

但是,在我看来,这只是这里的问题。只是这里的问题吗? (我们可以让instance volatile)。

【问题讨论】:

  • 你应该 google "双重检查锁定 Java" - 这不是用于单例的安全模式。
  • 请注意,我知道。我的问题是另一个。

标签: java volatile


【解决方案1】:

Shipilev 在Safe Publication and Safe Initialization in Java 中解释了您的示例。我强烈建议阅读整篇文章,但总结一下看看UnsafeLocalDCLFactory 部分:

public class UnsafeLocalDCLFactory implements Factory {
  private Singleton instance; // deliberately non-volatile

  @Override
  public Singleton getInstance() {
    Singleton res = instance;
    if (res == null) {
      synchronized (this) {
        res = instance;
        if (res == null) {
           res = new Singleton();
           instance = res;
        }
      }
    }
    return res;
  }
}

以上有以下问题:

这里引入局部变量是一种正确性修复,但只是部分修复:在发布 Singleton 实例和读取其任何字段之间仍然没有发生之前。我们只是保护自己不返回“null”而不是 Singleton 实例。同样的技巧也可以看作是 SafeDCLFactory 的性能优化,即只进行一次 volatile 读取,产生:

Shipilev 建议修复如下,将 instance 标记为 volatile:

public class SafeLocalDCLFactory implements Factory {
  private volatile Singleton instance;

  @Override
  public Singleton getInstance() {
    Singleton res = instance;
    if (res == null) {
      synchronized (this) {
        res = instance;
        if (res == null) {
          res = new Singleton();
          instance = res;
        }
      }
    }
    return res;
  }
}

这个例子没有其他问题。

【讨论】:

  • “这个例子没有其他问题。”。好吧,为什么?您能否指出 JMM 中的声明是如何成立的?
  • @KarolDowbecki 虽然我知道那篇文章 - 我在几个月内重新阅读它们一次,真正理解它们并不容易,Gilgamesz 在这里有一个很好的观点。我高度怀疑除了 SO 上的一些用户之外,有人真的可以解释这一点,我的意思是用简单的英语,它可能也需要很多页面
  • @Gilgamesz 这样想是绝对错误的,但是容易理解,writing一个volatile然后reading它,会引入必要的内存屏障,以便Singleton 的所有字段在被读取之前被写入,我试图在这里解释你的问题stackoverflow.com/questions/45151763/…
  • @Gilgamesz 还注意到,这很像冰上行走 - 即使我 认为 我对这些事情有基本的了解,我绝对不会在生产 - 我坚持 Shipilev 拥有的已知模式,再一次,SO 上只有少数用户真正了解这些东西......其中一个是 Shipilev 本人
  • 两年多之后我又一次偶然地阅读了这个答案,现在我更加确信这个答案并没有解决这个问题,我同意@Gilgamesz在那部分:“”没有此示例的其他问题。“。好的,为什么?”。你真的把希皮列夫的话断章取义了。 single volatile read 应该包含 racy 部分,否则在SafeLocalDCLFactory 中没有 single volatile read,但有单个 RACY 易失性读取。来自 OP 的问题继续提到 Singleton 的“一致性”——他的意思是如果
【解决方案2】:

通常我不会再使用双重检查锁定机制。要创建线程安全的单例,您应该让编译器这样做:

public class Factory {
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return res;
    }
}

现在您正在谈论使实例易失性。我认为这个解决方案没有必要这样做,因为 jit 编译器现在在构造对象时处理线程的同步。但是如果你想让它变得易变,你可以。

最后,我将 getInstance() 和实例设为静态。然后就可以直接引用Factory.getInstance(),不用构造Factory类。另外:您将在应用程序的所有线程中获得相同的实例。否则每个 new Factory() 都会给你一个新的实例。

您也可以查看维基百科。如果您需要惰性解决方案,他们有一个干净的解决方案:

https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java

// Correct lazy initialization in Java
class Foo {
    private static class HelperHolder {
       public static final Helper helper = new Helper();
    }

    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}

【讨论】:

  • 你没有回答这个问题。请注意,您的 Singleton 是最简单的:它不是懒惰的。
  • 我没有完整阅读一篇文章,但请注意一篇文章很老了。我想这篇文章已经过时了。
  • 我删除了这篇文章并添加了一个不同的解决方案,在维基百科上可以找到。
【解决方案3】:

编辑我又写了一个答案here,应该可以消除所有的困惑。

这是一个很好的问题,我将在这里尝试总结一下我的理解。

假设Thread1 当前正在初始化Singleton 实例并发布引用(显然不安全)。 Thread2 可以看到这个不安全的已发布引用(意味着它看到一个非空引用),但这并不意味着它通过该引用看到的字段(Singleton 通过构造函数初始化的字段)被初始化也正确。

据我所知,发生这种情况是因为可能会在构造函数内部对字段的存储进行重新排序。由于没有“先发生”规则(这些是普通变量),这完全有可能。

但这不是这里唯一的问题。请注意,您在此处进行了两次读取:

if (res == null) { // read 1

return res // read 2

这些读取没有同步保护,因此这些是 racy 读取。 AFAIK 这意味着允许读取 1 读取非空引用,而允许读取 2 读取空引用。

顺便说一句,ALL mighty Shipilev explains 是一样的东西(即使我读了这篇文章 1/2 年我仍然每次都能找到新的东西)。

确实创建实例volatile 会解决问题。当你让它变得易变时,就会发生这种情况:

 instance = res; // volatile write, thus [LoadStore][StoreStore] barriers

所有“其他”动作(从构造函数中存储)都不能通过这个栅栏,不会有重新排序。这也意味着当您读取 volatile 变量并看到一个非空值时,这意味着在写入 volatile 本身之前完成的每一次“写入”都已确定发生。 This excellent post has the exact meaning of it

这也解决了第二个问题,因为这些操作不能重新排序,所以保证您从read 1read 2 看到相同的值。

无论我阅读多少并试图理解这些东西对我来说总是很复杂,但我认识的很少有人可以编写这样的代码并正确推理。如果可以(我愿意!)请坚持使用已知且有效的双重检查锁定示例:)

【讨论】:

  • “这些读取没有同步保护,因此这些是活泼的读取。AFAIK 这意味着允许读取 1 读取非空引用,而允许读取 2 读取空引用。”没有比赛,因为instance只有一个读取。
  • 请注意:Singleton res = instancereturn res 之间存在数据依赖关系。因此,必须首先执行第一条语句。没有人(CPU、编译器、解释器等)无法进行重新排序,因为它由每个(几乎)CPU/内存模型保证。否则,我们的每一个程序都会被破坏。
  • 您是在说“请坚持使用已知且有效的双重检查锁定示例”。不,它并不总是有效,请参阅shipilev.net/blog/2014/safe-public-construction,查找“双重检查锁定习语”。
  • @Gilgamesz 关于您的第一条评论 - 当然有两个阅读,return res 在返回之前是 阅读 res
  • @Gilgamesz 关于你的第二条评论,好吧,如果你能听懂俄语就好了……youtube.com/watch?v=C6b_dFtujKo&t=2111s
【解决方案4】:

我是这样做的:

public class Factory {
    private static Factory factor;
    public static Factory getInstance() {
        return factor==null ? factor = new Factory() : factor;
    }
}

只是简单

【讨论】:

  • 这只有在Factory 不依赖于任何其他非“静态”属性时才有用;但即便如此,在这种情况下,一个简单的静态字段或枚举单例更好
【解决方案5】:

过了一段时间(是的,我知道花了 2 年时间),我想我有了正确的答案。从字面上看,答案是:

但是,在我看来,这只是这里的问题。只是这里有问题吗?

是的。按照你现在的方式,getInstance 的调用者永远不会看到null。但如果Singleton 有字段,则无法保证这些字段会正确初始化。

让我们慢慢来,因为这个例子很漂亮,恕我直言。您显示的代码执行单个(活泼)volatile read

public class Factory {
    private Singleton instance;
    public Singleton getInstance() {
        Singleton res = instance;      // <-- volatile RACY read
        if (res == null) {
            synchronized (this) {
                res = instance;        // <-- volatile read under a lock, thus NOT racy
                if (res == null) {
                    res = new Singleton();
                    instance = res;
                }
            }
        }
        return res;
    }
}

通常,经典的“双重检查锁定”有 两个 volatile 的活泼读取,例如:

public class SafeDCLFactory {

   private volatile Singleton instance;

   public Singleton get() {
     if (instance == null) {             // <-- RACY read 1 
       synchronized(this) {
         if (instance == null) {         // <-- non-racy read
            instance = new Singleton();
         }
       }
     }
     return instance;                    // <-- RACY read 2
   }
} 

因为这两个读取是活泼的,没有 volatile,所以这种模式被打破了。您可以阅读我们如何破解here, for example

在您的情况下,有一种优化,可以减少对 volatile 字段的读取。在某些平台上,这很重要,afaik。

问题的另一部分更有趣。如果Singleton 有一些我们需要设置的字段怎么办?

static class Singleton {
     
   //setter and getter also
   private Object obj;  
   
} 

还有一个工厂,Singleton volatile:

static class Factory {

    private volatile Singleton instance;

    public Singleton get(Object obj) {
        if (instance == null) {
            synchronized (this) {
                if (instance == null) {
                    instance = new Singleton();
                    instance.setObj(obj);
                }
            }
        }
        return instance;
    }
}

我们有一个不稳定的领域,我们是安全的,对吧?错误的。 obj 的赋值发生在易失性写入之后,因此不能保证它。简单的英语:this should help you a lot.

解决此问题的正确方法是使用已构建的实例(完全构建)进行 volatile 写入:

  if (instance == null) {
       Singleton local = new Singleton();
       local.setObj(obj);
       instance = local;
  }

【讨论】:

    【解决方案6】:

    现在,第二个线程可以读取不一致状态的实例。

    我很确定这确实是该代码中唯一的问题。我理解的方式,就行了

    instance = res;
    

    被执行,另一个线程可以读取instance 并将其视为非空,从而跳过synchronized。这意味着这两个线程之间没有发生之前的关系,因为只有当两个线程在同一个对象上同步或访问相同的易失性字段时才会存在这些关系。

    其他答案已经链接到Safe Publication and Safe Initialization in Java,它提供了以下解决不安全发布的方法:

    • 制作instance 字段volatile。所有线程都必须读取相同的volatile 变量,从而建立一个happens-before relation

      对 volatile 字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前。

    • 将单例包装到一个包装器中,该包装器将单例存储在final 字段中。 final 字段的规则没有像happens-before 关系那样正式指定,我能找到的最好的解释是final Field Semantics

      当一个对象的构造函数完成时,它被认为是完全初始化的。只能在对象完全初始化后才能看到对该对象的引用的线程可以保证看到该对象的 final 字段的正确初始化值。

      (不是对final字段的强调和限制,其他字段至少在理论上可能会处于不一致的状态)

    • 确保单例本身仅包含最终字段。解释同上。

    【讨论】:

      【解决方案7】:

      问题中提到的代码的问题是reordering可能会发生,并且线程可以获得单例类的部分构造对象。

      当我说reordering 时,我的意思是:

      public static Singleton getInstance() {
          if (instance == null) {
              synchronized (Singleton.class) {
                  if (instance == null) {
                      instance = new Singleton();
                      /* The above line executes the following steps:
                         1) memory allocation for Singleton class
                         2) constructor call ( it may have gone for some I/O like reading property file etc...
                         3) assignment ( look ahead shows it depends only on memory allocation which has already happened in 1st step. 
                         If compiler changes the order, it might assign the memory allocated to the instance variable. 
                         What may happen is half initialized object will be returned to a different thread )
                      */
                  }
              }
          }
          return instance;
      } 
      

      声明实例变量volatile确保上述3个步骤的happens-before/ordered关系:

      对 volatile 字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前。


      来自维基百科的Double-checked locking

      从 J2SE 5.0 开始,此问题已得到修复。 volatile 关键字现在可确保多个线程正确处理单例实例。 The "Double-Checked Locking is Broken" Declaration 中描述了这个新的成语:

      // Works with acquire/release semantics for volatile
      // Broken under Java 1.4 and earlier semantics for volatile
      class Foo {
          private volatile Helper helper = null;
          public Helper getHelper() {
              Helper result = helper;
              if (result == null) {
                  synchronized(this) {
                      result = helper;
                      if (result == null) {
                          helper = result = new Helper();
                      }
                  }
              }
              return result;
          }
      
          // other functions and members...
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-01-18
        • 1970-01-01
        • 1970-01-01
        • 2020-03-20
        • 1970-01-01
        • 2023-02-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多