出版和阅读非易变性领域

Gil*_*esz 13 java volatile

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

但是,就我而言,这只是问题所在.这只是问题吗?(而且我们可以变得instance不稳定).

Kar*_*cki 6

您可以通过安全发布中的Shipilev 和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;
  }
}
Run Code Online (Sandbox Code Playgroud)

以上有以下问题:

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

Shipilev建议通过标记instancevolatile 来解决如下问题:

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

这个例子没有其他问题.


Eug*_*ene 3

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

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

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

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

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

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

return res // read 2
Run Code Online (Sandbox Code Playgroud)

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

顺便说一句,这与所有强大的 Shipilev 解释的都是一样的(即使我 1/2 年读过一次这篇文章,我每次仍然会发现一些新东西)。

确实,举例volatile可以解决问题。当你让它变得不稳定时,会发生这种情况:

 instance = res; // volatile write, thus [LoadStore][StoreStore] barriers
Run Code Online (Sandbox Code Playgroud)

所有“其他”操作(来自构造函数内的存储)都无法通过此栅栏,因此不会重新排序。这也意味着当您读取 volatile 变量并看到非空值时,这意味着在写入 volatile 本身之前完成的每个“写入”都肯定发生了。这篇优秀的文章有它的确切含义

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

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