1

来自维基百科的规范破坏双重检查锁定的略微修改版本:

class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null) {

                    // Create new Helper instance and store reference on
                    // stack so other threads can't see it.
                    Helper myHelper = new Helper();

                    // Atomically publish this instance.
                    atomicSet(helper, myHelper);
                }
            }
        }
        return helper;
    }
}

假设底层原子操作库正常工作,是否简单地使新创建的 Helper 实例原子的发布使这个双重检查锁定习语安全?我意识到在 Java 中,可以只使用volatile,但即使该示例是伪 Java 中的,这也应该是一个与语言无关的问题。

也可以看看:

双重检查锁定文章

4

6 回答 6

14

这完全取决于您的平台/语言的确切内存模型。

我的经验法则:不要这样做。无锁(或减少锁,在这种情况下)编程很困难,除非您是线程忍者,否则不应尝试。您甚至应该仅在获得性能分析证明您确实需要它时才考虑它,在这种情况下,您可以获得关于该特定平台的线程的绝对最佳和最新的书籍,看看它是否可以帮助您。

于 2009-02-24T19:21:51.387 回答
7

我不认为你可以在不完全脱离代码的情况下以与语言无关的方式回答这个问题。这完全取决于您的伪代码的方式synchronizedatomicSet工作方式。

于 2009-02-24T19:14:26.207 回答
2

答案取决于语言 - 它归结为atomicSet().

如果 myHelper 的构造可以在之后展开,atomicSet()那么如何将变量分配给共享状态并不重要。

IE

// Create new Helper instance and store reference on
// stack so other threads can't see it.
Helper myHelper = new Helper(); // ALLOCATE MEMORY HERE BUT DON'T INITIALISE

// Atomically publish this instance.
atomicSet(helper, myHelper); // ATOMICALLY POINT UNINITIALISED MEMORY from helper

// other thread gets run at this time and tries to use helper object 

// AT THE PROGRAMS LEISURE INITIALISE Helper object.

如果语言允许这样做,那么双重检查将不起作用。

于 2009-02-24T19:23:08.153 回答
0

使用 volatile 不会阻止多个实例化 - 但是使用 synchronize 会阻止创建多个实例。但是,对于您的代码,可能会在设置之前返回助手(线程'A'实例化它,但在设置线程'B'出现之前,助手是非空的,因此立即返回它。修复那个问题,去掉第一个 if (helper == null)。

于 2009-02-24T19:22:29.027 回答
0

很可能它被破坏了,因为部分构造的对象的问题没有得到解决。

于 2009-02-24T19:25:27.150 回答
0

对于所有担心部分构造对象的人:

据我了解,部分构造对象的问题只是构造函数内部的问题。换句话说,在构造函数中,如果一个对象引用了自己(包括它的子类)或其成员,那么部分构造可能会出现问题。否则,当构造函数返回时,该类已完全构造。

我认为您将部分构造与编译器如何优化写入的不同问题混淆了。编译器可以选择 A) 为新的 Helper 对象分配内存,B) 将地址写入 myHelper(本地堆栈变量),然后 C) 调用任何构造函数初始化。在 B 点之后和 C 点之前的任何时候,访问 myHelper 都是一个问题。

引用的论文关注的是这种编译器的写入优化,而不是部分构造。在原来的单检查锁方案中,优化写入可以让多个线程看到B点和C点之间的成员变量。这种实现通过使用局部堆栈变量来避免写入优化问题。

引用论文的主要范围是描述双重检查锁解决方案的各种问题。但是,除非 atomicSet 方法也与 Foo 类同步,否则此解决方案不是双重检查锁定解决方案。它正在使用多个锁。

我想说这一切都归结为原子分配函数的实现。该函数需要是真正原子的,它需要保证处理器本地内存缓存是同步的,并且它需要以比简单地始终同步 getHelper 方法更低的成本来完成所有这些工作。

根据引用的论文,Java 不太可能满足所有这些要求。此外,论文中应该非常清楚的一点是 Java 的内存模型经常变化。它随着对缓存、垃圾收集等的更好理解的发展而适应,并适应虚拟机运行的底层真实处理器架构的变化。

根据经验,如果您以依赖于底层实现而不是 API 的方式优化 Java 代码,那么您将面临在 JVM 的下一个版本中出现代码损坏的风险。(虽然,有时你别无选择。)


dsimcha:

如果您的 atomicSet 方法是真实的,那么我会尝试将您的问题发送给 Doug Lea(连同您的 atomicSet 实现)。我感觉他是那种会回答的人。我猜想对于 Java,他会告诉您始终同步并在其他地方进行优化会更便宜。

于 2009-02-25T22:12:34.697 回答