Java双重检查锁定

时间:2009-10-26 14:24:24

标签: java multithreading synchronization singleton double-checked-locking

我最近发表了一篇文章,讨论了Java中的双重检查锁定模式及其缺陷,现在我想知道我多年来一直使用的那种模式的变体是否存在任何问题。

我查看了很多关于这个主题的帖子和文章,并了解了获取部分构造对象的引用的潜在问题,据我所知,我不我的实施受这些问题的影响。以下模式是否存在任何问题?

如果没有,为什么人们不使用它?我在这个问题的任何讨论中都没有看过它。

public class Test {
    private static Test instance;
    private static boolean initialized = false;

    public static Test getInstance() {
        if (!initialized) {
            synchronized (Test.class) {
                if (!initialized) {
                    instance = new Test();
                    initialized = true;
                }
            }
        }
        return instance;
    }
}

11 个答案:

答案 0 :(得分:25)

Double check locking is broken。由于initialized是一个原语,因此它可能不需要它是易失性的,但是在初始化实例之前,没有什么能阻止初始化被视为对非同步代码的真实。

编辑:为了澄清上述答案,原始问题询问是否使用布尔值来控制双重检查锁定。如果没有上面链接中的解决方案,它将无法正常工作。您可以仔细检查实际设置布尔值的锁定,但在创建类实例时仍然存在有关指令重新排序的问题。建议的解决方案不起作用,因为在非同步块中看到初始化的布尔值为true后,实例可能无法初始化。

双重检查锁定的正确解决方案是使用volatile(在实例字段上)并忘记初始化的布尔值,并确保使用JDK 1.5或更高版本,或者在最终字段中初始化它,如详细说明在链接的文章和汤姆的回答中,或者只是不要使用它。

当然,整个概念似乎是一个巨大的过早优化,除非你知道你将获得大量的线程争用获得这个Singleton,或者你已经分析了应用程序并且已经看到这是一个热点。

答案 1 :(得分:16)

如果initializedvolatile,则会有效。与synchronized一样,volatile的有趣效果与参考文献的关系并不如我们对其他数据的说法那么多。设置instance字段和Test对象将被强制发生在写入initialized之前。通过短路使用缓存值时,initialize读取发生在<{em>}读取instance之前和通过引用到达的对象。单独的initialized标志没有显着差异(除了它会导致代码更加复杂)。

(不安全发布的构造函数中final字段的规则略有不同。)

但是,在这种情况下,您应该很少看到错误。第一次使用时遇到麻烦的可能性很小,而且这是一次非重复比赛。

代码过于复杂。你可以把它写成:

private static final Test instance = new Test();

public static Test getInstance() {
    return instance;
}

答案 2 :(得分:12)

双重检查锁定确实被破坏了,问题的解决方案实际上比这个习惯用法更简单 - 只需使用静态初始化程序。

public class Test {
    private static final Test instance = createInstance();

    private static Test createInstance() {
        // construction logic goes here...
        return new Test();
    }

    public static Test getInstance() {
        return instance;
    }
}

静态初始化程序保证在JVM第一次加载类时执行,并且在类引用可以返回到任何线程之前执行 - 使其本身具有线程安全性。

答案 3 :(得分:5)

这就是双重检查锁定被破坏的原因。

同步保证,只有一个线程可以输入代码块。但它并不能保证在同步部分内完成的变量修改对其他线程是可见的。只有进入同步块的线程才能保证看到更改。这就是为什么双重检查锁定被打破的原因 - 它在读者方面没有同步。读取线程可以看到单例不为空,但单例数据可能未完全初始化(可见)。

订购由volatile提供。 volatile保证排序,例如写入volatile单例静态字段保证在写入易失性静态字段之前完成对单例对象的写入。它不会阻止创建两个对象的单例,这是通过同步提供的。

类最终静态字段不需要是volatile。在Java中,JVM解决了这个问题。

请参阅我的帖子an answer to Singleton pattern and broken double checked locking in a real-world Java application,其中说明了一个关于双重检查锁定的单例示例,它看起来很聪明但是已经坏了。

答案 4 :(得分:0)

您应该使用java.util.concurrent.atomic中的原子数据类型。

答案 5 :(得分:0)

如果“initialized”为真,那么“instance”必须完全初始化,与1加1等于2 :)相同。因此,代码是正确的。该实例仅被实例化一次,但该函数可能被调用一百万次,因此它确实提高了性能,而无需检查同步百万减去一次。

答案 6 :(得分:0)

在某些情况下,可能会使用双重检查。

  1. 首先,如果你真的不需要单身,那么双重检查仅用于NOT创建和初始化为许多对象。
  2. 在构造函数/初始化块的末尾设置了final字段(导致其他线程看到所有先前初始化的字段)。

答案 7 :(得分:0)

我一直在调查双重检查锁定习惯用法,根据我的理解,你的代码可能会导致读取部分构造的实例的问题,除非你的Test类是不可变的:

  

Java Memory Model为共享不可变对象提供了初始化安全性的特殊保证。

     

即使不使用同步来发布对象引用,也可以安全地访问它们。

(非常可取的书籍Java Concurrency in Practice中的引文)

因此,在这种情况下,双重检查的锁定习惯用法会起作用。

但是,如果不是这种情况,请注意您在没有同步的情况下返回变量实例,因此可能无法完全构造实例变量(您将看到属性的默认值,而不是构造函数中提供的值) )。

布尔变量没有添加任何东西来避免这个问题,因为在初始化Test类之前它可能被设置为true(synchronized关键字不能完全重新排序,一些sencences可能会改变顺序)。 Java内存模型中没有事先发生的规则来保证这一点。

并且使布尔volatile也不会添加任何东西,因为32位变量是在Java中原子创建的。双重检查锁定习语也适用于它们。

从Java 5开始,您可以解决将实例变量声明为volatile的问题。

您可以在this very interesting article中详细了解双重检查的习语。

最后,我读过一些建议:

  • 考虑是否应该使用单例模式。它被许多人认为是一种反模式。在可能的情况下,首选依赖注入。查看this

  • 在实施之前,请仔细考虑是否真的需要双重检查锁定优化,因为在大多数情况下,这不值得付出努力。另外,考虑在静态字段中构造Test类,因为延迟加载仅在构造类占用大量资源时才有用,而且在大多数情况下,情况并非如此。

如果您仍然需要执行此优化,请查看此link,它提供了一些替代方案,以实现与您尝试的类似效果。

答案 8 :(得分:0)

DCL问题已经破裂,即使它似乎适用于许多虚拟机。这里有一个很好的关于这个问题的文章http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html

  

多线程和内存一致性是比它们可能出现的更复杂的主题。 [...]如果您只是使用Java为此目的提供的工具 - 同步,您可以忽略所有这些复杂性。 如果您同步对可能已经写入或可以被另一个线程读取的变量的每次访问,您将没有内存一致性问题。

正确解决此问题的唯一方法是避免延迟初始化(急切地执行)或在同步块内单独检查。使用布尔值initialized相当于对引用本身进行空检查。第二个线程可能会看到initialized为true,但instance可能仍为null或部分初始化。

答案 9 :(得分:0)

双重检查锁定是反模式。

延迟初始化持有者类是您应该关注的模式。

尽管还有很多其他的答案,我想我应该回答,因为仍然没有一个简单的答案说明为什么DCL在很多情况下会被打破,为什么它是不必要的以及你应该做什么。所以我将使用来自Goetz: Java Concurrency In Practice的引用,对我来说,它在Java内存模型的最后一章中提供了最简洁的解释。

关于变量的安全发布:

  

DCL的真正问题是假设在没有同步的情况下读取共享对象引用时可能发生的最糟糕的事情是错误地看到过时的值(在这种情况下,为null);在这种情况下,DCL惯用法通过再次尝试锁定来补偿这种风险。但最坏的情况实际上要差得多 - 有可能看到参考的当前值但对象状态的陈旧值,这意味着可以看到对象处于无效或不正确的状态。

     

JMM(Java 5.0及更高版本)中的后续更改使DCL在资源变为易失性时能够正常工作,并且这对性能的影响很小,因为易失性读取通常仅比非易失性读取稍贵。

     

然而,这是一个习惯性已经基本消失的习惯用语 - 激发它的力量(缓慢的无竞争同步,慢速JVM启动)不再起作用,使其作为优化效率降低。懒惰的初始化持有者习语提供了相同的好处,并且更容易理解。

     

清单16.6。 Lazy Initialization Holder Class Idiom。

public class ResourceFactory
    private static class ResourceHolder {
        public static Resource resource = new Resource();
    }

    public static Resource getResource() {
        return ResourceHolder.resource;
    }
}

这是做到这一点的方法。

答案 10 :(得分:0)

首先,对于单身人士,您可以使用Enum,正如此问题中所述Implementing Singleton with an Enum (in Java)

其次,从Java 1.5开始,您可以使用具有双重检查锁定的volatile变量,如本文末尾所述:https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html