【问题标题】:Is this a safe version of double-checked locking?这是双重检查锁定的安全版本吗?
【发布时间】:2010-01-13 01:13:15
【问题描述】:

这是我刚刚想出的一个想法,它是一种安全、有效的方法来处理单例同步问题。它基本上是双重检查锁定,但有一个涉及线程本地存储的扭曲。在 Java/C#/D 风格的伪代码中,假设 __thread 表示静态变量的线程本地存储:

class MySingleton {
    __thread static bool isInitialized; 
    static MySingleton instance; 

    static MySingleton getInstance() {
        if(!isInitialized) {
            synchronized {
                isInitialized = true;
                if(instance is null) {
                    instance = new MySingleton();
                }
            }
        }

        return instance;
   }
}

这保证在程序的整个生命周期中每个线程只进入synchronized 块一次。从那时起,我们对线程局部布尔值进行简单检查,以查看我们是否已经进入同步块并验证对象是从该线程初始化的。

【问题讨论】:

  • 我看不到线程存储给聚会带来的任何好处
  • 在实践中,线程本地存储可能比简单的同步涉及更多的开销。
  • 并且每个线程进入一次。
  • 而且 TLS 通常比其他同步指令(甚至 cmpxchg 等)更快,如果它是编译器固有的 TLS(即使用 __thread 或 declspec(__thread) 或任何您的语言使用)。相反,像 pthreads 这样的通用 TLS 实现可能会更慢。查看来自 Google 的 Mike Burrows 的类似想法。 open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2444.html
  • 即使是非内在线程本地存储也可能通常更快,因为它不涉及原子操作。在我的 Windows 机器上,TlsGetValue(),一个 Win32/64 API 调用在大约 7-8 个周期内返回,而单个 cmpxchg 指令大约需要 30 个周期。

标签: multithreading language-agnostic concurrency locking thread-safety


【解决方案1】:

我认为与语言无关(或与平台无关)的方法在这里没有用处,因为虽然结构在逻辑上可能是合理的,但存在特定于实现的陷阱会阻止它正常工作。一个例子是双重检查锁定,它在 Java pre-5 上不起作用,因为它在 JVM 级别被破坏。

因此,您应该在每个平台上使用可用的语言结构或库。

对于 Java,您可以使用 enum 获取单例。

【讨论】:

  • java 中的枚举单例非常漂亮。
【解决方案2】:

这看起来很干净。实例对象的实际检查和初始化是在一个同步块内,每个线程在第一次调用时被强制进入同步块,你会在线程之间获得一个干净的发生前边缘。

既然 isInitialized 是线程本地的,为什么要在同步块中设置它?此外,您应该只在构造单例对象后设置 isInitalized。这样,如果它还没有被初始化并且构造函数抛出,这个线程将在下次调用时再次检查。

    if(!isInitialized) {
        synchronized {
            if(instance is null) {
                instance = new MySingleton();
            }
        }
        isInitialized = true;
    }

【讨论】:

  • 接受,因为您对如何改进这个成语提出了很好的建议。
【解决方案3】:

双重检查锁定被破坏的原因(据我所知)是instance 不为空但由于读/写重新排序而未完全构造的可能性。

线程本地存储解决不了任何问题。它可能会让您不必将 isInitialized 声明为 volatile,但这仍然不能解决您的问题。

【讨论】:

  • instance 始终分配有持有的锁。任何特定线程第一次读取instance 时,锁也会被持有,因此部分构造没有问题。
  • 可能适用于 java 6+,但对于 .NET 则不是很多...尽管在 x86 上使用 CLR 你没问题。这个博客解释它:bluebytesoftware.com/blog/…
  • 在我之前的评论中应该是 java 5+。
【解决方案4】:

是的,这个结构在我所知道的所有高级语言下都是安全的。特别是,在内存/并发模型保证给定线程始终以与程序顺序一致的顺序(这几乎是任何有用的语言)看到它的 自己的 操作的任何语言中,它都是安全的,并且其中同步块或等效块提供了关于块之前、内部和之后的操作的通常保证。

【讨论】:

    【解决方案5】:

    就 Java 而言,我觉得不错。如果您打算这样做,则更常规的做法是使线程本地存储成为引用。阅读静态也没有意义。

    但是在 Java 中类加载是惰性的并且是线程安全的,所以你不妨写:

    private static final MySingleton instance = new MySingleton(); 
    

    或者根本不使用单例。

    【讨论】:

      【解决方案6】:

      是的,在新的 jdk5 JMM 下,如果您声明实例 volatile DCL 将起作用

      【讨论】:

      • 另外,处理单例的最佳方法是使用 IODH 或枚举(如该线程中所述)
      • @Loop 如果您正在初始化一个 volatile 对象,然后引用该对象的非 volatile 字段,该字段在构造函数中已初始化,会发生什么情况。您引用该 objects 字段,您仍然可以拥有一个部分构造的对象。 Volatile 确实修复了您正在创建的一个对象,但没有修复它的嵌套对象。
      • John,这就是 volatile 的重点:它可以防止另一个线程出现并在实例完成初始化之前读取它。这样就解决了部分构造对象的 DCL 问题。检查 volatile 数组的语义:jeremymanson.blogspot.com/2009/06/volatile-arrays-in-java.html
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多