当前位置: 首页 > 知识库问答 >
问题:

非易失性字段的发布和读取

谷梁博易
2023-03-14
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;
    }
}

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

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

但是,在我看来,这只是问题。这是唯一的问题吗?(并且我们可以使实例不稳定)。

共有3个答案

卞浩漫
2023-03-14

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

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

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

就我所见,之所以会发生这种情况,是因为构造函数中可能会对字段的存储进行重新排序。因为没有“之前发生”的规则(这些都是简单的变量),所以这是完全可能的。

但这并不是唯一的问题。请注意,这里有两种解读:

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

return res // read 2

这些读取没有同步保护,因此它们是快速读取。AFAIK这意味着read 1可以读取非空引用,而read 2可以读取空引用。

顺便说一句,这和全能的希皮尔耶夫解释的是一样的(即使我读了这篇文章1/2年,我仍然每次都能找到新的东西)。

事实上,让实例变得不稳定将解决问题。当你让它变得不稳定时,就会发生这种情况:

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

所有“其他”操作(来自构造函数内的存储)都不能通过此界限,不会重新排序。这还意味着,当您读取volatile变量并看到一个非空值时,这意味着在写入volatile之前所做的每一次“写入”都确实发生了。这篇优秀的帖子有它的确切含义

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

无论我读了多少书,并试图理解这些东西对我来说总是很复杂,我所认识的人中很少有人能够编写这样的代码,并正确地对此进行推理。当你可以的时候(我愿意!)请坚持使用已知和有效的双重检查锁定示例:)

公孙芷阳
2023-03-14

通常情况下,我再也不会使用双重检查锁定机制了。要创建线程安全的单例,您应该让编译器执行以下操作:

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

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

最后,我将使getInstance()和实例保持静态。然后你可以参考工厂。直接获取实例(),而不构造工厂类。另外:应用程序中的所有线程都将获得相同的实例。否则每个新工厂()都会给你一个新实例。

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

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;
    }
}
苍德寿
2023-03-14

ShiPilev在Java的安全发布和安全初始化中解释了您的示例。我强烈建议阅读整篇文章,但是要总结一下,请查看那里的UnsecLocalDCLFactory部分:

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”而不是单例实例。同样的技巧也可以被视为SafeDCLFactory的性能优化,即只进行一次易失性读取,产生:

ShiPilev建议通过标记实例来修复如下问题:

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;
  }
}

这个例子没有其他问题。

 类似资料:
  • 在阅读了这个问题和这个(尤其是第二个答案)之后,我对volatile及其关于记忆障碍的语义感到非常困惑。 在上面的例子中,我们写入一个易失性变量,这会导致一个mitch,这反过来会将所有挂起的存储缓冲区/加载缓冲区刷新到主缓存,使其他缓存行无效。 然而,非易失性字段可以优化并存储在寄存器中,例如?那么,我们如何才能确保给定一个写入易失性变量之前的所有状态变化都是可见的呢?如果我们有1000件东西呢

  • 问题内容: 我尝试了解为什么此示例是正确同步的程序: 由于存在冲突的访问(存在对a的写入和读取),因此在每个顺序一致性中,必须在访问之间的关系之前执行。假设顺序执行之一: 是1发生-在2之前发生,为什么? 问题答案: 不,在相同变量的易失性写入之前(以同步顺序),在易失性写入 之前 不一定 会发生 易失性读取。 这意味着它们可能处于“数据争用”中,因为它们“冲突的访问未按先发生后关系进行排序”。如

  • 我对线程的概念还是相当陌生的,并试图对它有更多的了解。最近,我看到Jeremy Manson写的一篇关于Volatile在Java中的含义的博文,他写道: 当一个线程写入一个易失性变量,而另一个线程看到该写操作时,第一个线程将告诉第二个线程内存中的所有内容,直到它执行了对该易失性变量的写操作为止。[...] 线程1在写入之前看到的所有内存内容,在读取的值之后,必须对线程2可见。[我自己加的重点]

  • 我对下面的代码段有一个问题。结果可能有一个结果[0,1,0](这是用JCStress执行的测试)。那么这是怎么发生的呢?我认为数据写入(data=1)应该在Actor2(guard2=1)中写入到guard2之前执行。我说得对吗?我问,因为很多时候我读到挥发物周围的说明没有重新排序。此外,根据这一点:http://tutorials.jenkov.com/java-concurrency/vola

  • 如果是,如果被添加到数组中,强制转换会受到什么影响?谢谢你! 编辑:@cacahuete Frito链接了一个非常相似的问题:带有'volatile'数组的'memcpy((void*)dest,src,n)'安全吗?

  • 当我在jdk1.8中阅读时,我看到方法具有易失性读写的内存语义学的注释。 注释和代码如下: 在AbstractQueuedSynchronizer类中,是一个名为 只是想知道如果是一个非易失性成员,记忆语义学是什么。