在Head First设计模式手册中,具有双重检查锁定的单例模式已实现如下:

public class Singleton {
    private volatile static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}


我不明白为什么要使用volatilevolatile用法是否违反使用双重检查锁定(即性能)的目的?

评论

我以为双重检查锁定已损坏,有人修复了吗?

值得一提的是,我发现Head First设计模式是一本可怕的书,值得学习。当我回顾它时,既然我已经在其他地方学习了模式,那将是非常有意义的,但是在不了解模式的情况下进行学习实际上并没有达到其目的。但这很受欢迎,所以也许只是我很稠密。 :-)

@DavidHeffernan我已经看到了这个示例,它是信任jvm进行DCL的一种方式。

FWIW,在x86系统上,易失性Read-Read应该导致无操作。实际上,唯一需要围墙以确保内存一致性的操作就是易失性的读写操作。因此,如果您实际上只将值写入一次,那么影响应该最小。我还没有看到有人实际对此进行过基准测试,并认为结果会很有趣!

出于所有实际原因,@ DavidHeffernan仍然存在问题。最好使用volatile(例如,“不会破坏程序”),但那样的话,您赢的并不多。

#1 楼

JCIP书籍提供了一个很好的资源,可以理解为什么需要volatile。 Wikipedia也对该材料进行了不错的解释。

真正的问题是Thread A可能在完成instance的构造之前为其分配了一个内存空间。 instance将看到该分配并尝试使用它。这导致Thread B失败,因为它使用的是Thread B的部分构造版本。

评论


好的,它看起来像是volatile的新实现,解决了DCL的内存问题。我仍然没有得到的是这里使用volatile的性能含义。从我所读的内容中,volatile与同步几乎一样慢,那么为什么不同步整个getInstance()方法调用呢?

–toc777
2011年10月21日在22:39

@ toc777 volatile比通常提交的要慢。如果您需要性能,请选择持有人级别的模式。 volatile在这里只是表明有一种方法可以使折断的模式起作用。与其说是实际问题,不如说是编码挑战。

– alf
2011年10月21日在22:43



@Tim好,XML中的单例仍然是单例;通过使用DI来了解应用程序的运行时状态并不容易。较小的代码单元似乎更简单,但要以强制构造所有单元以符合DI惯用语为代价(有人可能会说这是一件好事)。针对单例的指控是不公平的,它使API与impl混淆了-Foo.getInstance()只是以某种方式获取Foo的表达式,它与@Inject Foo foo没有什么不同;无论哪种方式,请求Foo的站点都不知道返回哪个Foo,以及静态和运行时依赖性如何相同。

–信誉不佳
11-10-22在3:18

@irreputable您知道有趣的是,在我们进行这次交流时,我从未使用过Spring,也没有在指代Spring的不稳定DI。将Singleton用作静态工厂的真正危险是试图将其称为深入内部代码,这种代码不应了解getInstance()方法,而是要求提供实例。

– Tim Bender
13年6月8日在2:05

真正的问题是线程A可能在完成构造实例之前就分配了一个内存空间。那么如何解决这个问题呢?

– shaoyihe
18-10-31在5:24



#2 楼

正如@irreputable所引用的那样,volatile并不昂贵。即使价格昂贵,一致性也应优先于性能。

对于懒惰的单身人士,还有一种更简洁的优雅方式。

public final class Singleton {
    private Singleton() {}
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}


源文章:来自Wikipedia的Initial-on-demand_holder_idiom


在软件工程中,按需初始化持有人(设计模式)惯用语是一个延迟加载的单例。在Java的所有版本中,该惯用法都可以实现安全,高度并发的延迟初始化,并具有良好的性能。由于该类没有任何static变量可进行初始化,因此该初始化很容易完成。

JVM确定必须执行LazyHolder之前,不会初始化其中的静态类定义LazyHolder

仅当调用静态方法LazyHolder时,才执行静态类getInstance

此解决方案是线程安全的,不需要特殊的语言构造(例如,LazyHoldervolatile)。

#3 楼

好吧,没有针对性能的双重检查锁定。这是一种破碎的模式。

撇开情绪,这里是因为在第二个线程通过volatile时没有它,第一个线程可能还无法构造instance == null:没有人保证会创建对象-在为任何线程(实际上创建对象的线程)分配给new Singleton()之前。

instance依次建立读写之间的先于关系,并修复损坏的模式。

如果您要寻找性能,请改为使用Holder内部静态类。

评论


嗨@alf,根据事前定义,在一个线程释放锁之后,另一个线程获得了锁,然后后者可以看到之前的更改。如果是这样,我认为不需要volatile关键字。你能详细解释一下吗

–秦冬亮
18年7月5日在2:21



但是,如果第二个线程仅在外部命中外部,则没有锁获取,因此没有排序。

– alf
18年7月5日在8:47

@alf,您好,您是否试图指出,当第一个线程在同步块内创建实例时,该实例对于第二个线程可能仍然为null,因为高速缓存未命中,如果实例不是易失性,它将再次实例化?你能澄清一下吗?

–Aarish Ramesh
7月26日14:57

@AarishRamesh,不为null;任何状态。有两个操作:为实例变量分配地址,以及在该地址处实际创建对象。除非有强制执行同步的操作(如易失性访问或显式同步),否则第二个线程可能会使这两个事件混乱。

– alf
7月27日15:55



#4 楼

如果没有它,则在第一个将同步块设置为null后,第二个线程可能会进入同步块,并且本地缓存仍会认为它为null。

第一个线程不是正确性(如果您是对的,那将是自我挫败),而是为了优化。

评论


根据事前定义,在一个线程释放锁之后,另一个线程获得了锁,然后后者可以看到先前的更改。如果是这样,我认为不需要volatile关键字。你能详细解释一下吗

–秦冬亮
18年7月5日在2:32

@QinDongLiang如果变量不是volatile,则第二个线程可能正在其自己的堆栈上使用缓存的值。不稳定会迫使它回到源头以获取适当的值。当然,它必须在每次访问时都这样做,因此可能会影响性能(但是,老实说,除非它处于超临界循环中,否则可能不是系统中最糟糕的事情……)

– corsiKa
18年7月5日在3:43

#5 楼

将变量声明为volatile可以保证对它的所有访问实际上都从内存中读取其当前值。

如果没有volatile,编译器可能会优化对变量的内存访问(例如将其值保留在寄存器中) ),因此只有第一次使用该变量时,才会读取保存该变量的实际内存位置。如果变量在第一次访问和第二次访问之间被另一个线程修改,则会出现问题。第一个线程只有第一个(预修改的)值的副本,因此第二个if语句测试变量值的陈旧副本。

评论


-1我今天失去了声誉:)真正的原因是,存在内存缓存,其建模为线程的本地内存。本地存储器刷新到主存储器的顺序是不确定的-也就是说,除非您发生先于关系,例如通过使用volatile。寄存器与不完整的对象和DCL问题无关。

– alf
2011-10-21 22:14



您对volatile的定义太狭窄了-如果所有这些都是volatile,那么在
– Voo
2011-10-21 22:18



@TimBender如果单例包含可变状态,则刷新它与对单例本身的引用无关(嗯,有一个间接链接,因为访问对单例的volatlie引用会使您的线程重新读取主内存,但它是辅助内存)效果,而不是问题的原因:))

– alf
2011-10-21 22:18



@alf,您是对的。实际上,如果内部状态可变,则使实例易失性无济于事,因为刷新仅在引用本身发生更改时发生(例如使数组/列表易失性对内容不起作用)。将其粉化成脑屁。

– Tim Bender
2011年10月21日在22:22

根据事前定义,在一个线程释放锁之后,另一个线程获得了锁,然后后者可以看到先前的更改。如果是这样,我认为不需要volatile关键字。您能详细解释吗@Tim Bender

–秦冬亮
18年7月5日在2:35

#6 楼

易失性读取本身并不真正昂贵。

您可以设计一个测试以紧密循环调用getInstance(),以观察易失性读取的影响。但是,这种测试是不现实的;在这种情况下,程序员通常会调用一次getInstance()并在使用期间缓存实例。

另一个暗示是通过使用final字段(请参阅维基百科)。这需要额外的读取,可能会比volatile版本贵。 final版本在紧密循环中可能会更快,但是如前所述,该测试尚无定论。

#7 楼

双重检查锁定是一种在多线程环境中调用getInstance方法时防止创建另一个单例实例的技术。

请注意



检查单个实例
仅在首先检查单例实例后才使用同步关键节,因为该原因可提高性能。
在实例成员的声明中使用volatile关键字。这将告诉编译器始终从主内存而不是从CPU高速缓存读取和写入。使用volatile变量保证事前发生关系,所有写操作都将在实例变量的任何读取之前发生。

缺点


由于需要volatile关键字才能起作用正确地讲,它与Java 1.4及更低版本不兼容。问题是无序写入可能允许实例引用在执行单例构造函数之前返回。
性能问题是由于易失性变量的递减缓存。
在初始化之前先检查了两次实例。
它很冗长,使代码难以阅读。

单例模式有几种实现,各有优缺点。


渴望加载单例
双重检查锁定单例
按需初始化持有人习惯用法
基于枚举的单例

每个人的详细说明都太冗长,因此我只链接了一篇好文章-您想了解的有关Singleton的所有信息