在Java中实现单例模式的有效方法是什么?

在Java中实现单例设计模式的有效方法是什么?

330341 次浏览

确保你真的需要它。在谷歌上搜索“单例反模式”以查看一些反对它的论点。

我想它本身并没有什么问题,但它只是一种暴露一些全局资源/数据的机制,因此请确保这是最好的方式。特别是,我发现依赖注入(DI)更有用,特别是如果您还使用单元测试,因为DI允许您将模拟资源用于测试目的。

维基百科有一些示例的单例,也在Java中。Java5实现看起来很完整,并且是线程安全的(应用了双重检查锁定)。

在写之前要认真考虑为什么你需要一个单例。关于使用它们有一个准宗教的争论,如果你在Java中谷歌单例,你很容易绊倒。

就我个人而言,我试图尽可能多地避免单例,原因有很多,其中大部分都可以通过谷歌搜索单例找到。我觉得单例经常被滥用,因为它们很容易被每个人理解。它们被用作将“全局”数据纳入OO设计的机制,并且它们被使用是因为很容易规避对象生命周期管理(或者真正思考如何从B内部做A)。看看控制反转(IoC)或依赖注入(DI)这样的中景。

如果你真的需要一个,那么维基百科有一个正确实现单例的好例子。

忘记延迟初始化;这太麻烦了。这是最简单的解决方案:

public class A {
private static final A INSTANCE = new A();
private A() {}
public static A getInstance() {return INSTANCE;}}

如果您不需要延迟加载,那么只需尝试:

public class Singleton {private final static Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return Singleton.INSTANCE; }
protected Object clone() {throw new CloneNotSupportedException();}}

如果您想要延迟加载并且希望您的单例是线程安全的,请尝试双重检查模式:

public class Singleton {private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {if(null == instance) {synchronized(Singleton.class) {if(null == instance) {instance = new Singleton();}}}return instance;}
protected Object clone() {throw new CloneNotSupportedException();}}

由于双重检查模式不能保证工作(由于编译器的一些问题,我对此一无所知),您还可以尝试同步整个getInstans-方法或为所有单例创建一个注册表。

线程安全在Java5+:

class Foo {private static volatile Bar bar = null;public static Bar getBar() {if (bar == null) {synchronized(Foo.class) {if (bar == null)bar = new Bar();}}return bar;}}

注意这里的volatile修饰符。:)这很重要,因为没有它,JMM(Java内存模型)不能保证其他线程看到对其值的更改。同步不要负责-它只序列化对该代码块的访问。

@Bno的回答详细介绍了Bill Pugh(FindBugs)推荐的方法,并且可以更好地争论。去阅读并投票支持他的答案。

使用枚举:

public enum Foo {INSTANCE;}

Joshua Bloch在Google I/O 2008:视频链接有效Java重装演讲中解释了这种方法。另请参阅他演讲的幻灯片30-32(effective_java_reloaded.pdf):

实现可序列化单例的正确方法

public enum Elvis {INSTANCE;private final String[] favoriteSongs ={ "Hound Dog", "Heartbreak Hotel" };public void printFavorites() {System.out.println(Arrays.toString(favoriteSongs));}}

编辑:“有效Java”的在线部分说:

"这种方法在功能上等同于公共字段方法,只是它更简洁,免费提供序列化机制,并提供了防止多次实例化的铁定保证,即使面对复杂的序列化或反射攻击。虽然这种方法尚未被广泛采用,单元素枚举类型是实现单例的最佳方式."

我对一些建议依赖注入(DI)作为使用单例替代的答案感到困惑;这些是不相关的概念。您可以使用DI注入单例或非单例(例如,每个线程)实例。至少如果您使用Spring 2. x,这是真的,我不能为其他DI框架说话。

所以我对OP的回答是(除了最琐碎的示例代码之外):

  1. 使用像Spring框架这样的DI框架,然后
  2. 使其成为DI配置的一部分,无论您的依赖项是单例、请求范围、会话范围还是其他什么。

这种方法为您提供了一个很好的解耦(因此灵活且可测试)架构,其中是否使用单例是一个容易可逆的实现细节(当然,如果您使用的任何单例都是线程安全的)。

由Stu Thompson发布的解决方案在Java5.0及更高版本中是有效的。但我宁愿不使用它,因为我认为它很容易出错。

人们很容易忘记易失性语句,却很难理解为什么需要它。如果没有易失性,由于双重检查锁定反模式,这段代码将不再是线程安全的。更多信息请参阅Java实践中的并发的第16.2.4段。简而言之:这种模式(在5.0Java之前或没有易失性语句)可能会返回对(仍然)处于不正确状态的Bar对象的引用。

这种模式是为了性能优化而发明的。但这真的不再是一个真正的问题了。以下懒惰初始化代码速度很快,更重要的是更容易阅读。

class Bar {private static class BarHolder {public static Bar bar = new Bar();}
public static Bar getBar() {return BarHolder.bar;}}

根据用法不同,有几个“正确”答案。

从Java5开始,最好的方法是使用枚举:

public enum Foo {INSTANCE;}

Java5,最简单的例子是:

public final class Foo {
private static final Foo INSTANCE = new Foo();
private Foo() {if (INSTANCE != null) {throw new IllegalStateException("Already instantiated");}}
public static Foo getInstance() {return INSTANCE;}
public Object clone() throws CloneNotSupportedException{throw new CloneNotSupportedException("Cannot clone instance of this class");}}

让我们回顾一下代码。首先,你希望类是最终的。在这种情况下,我使用了final关键字让用户知道它是最终的。然后你需要将构造函数设为私有,以防止用户创建自己的Foo。从构造函数抛出异常可防止用户使用反射来创建第二个Foo。然后你创建一个private static final Foo字段来保存唯一的实例,并创建一个public static Foo getInstance()方法来返回它。Java规范确保只有在首次使用类时才调用构造函数。

当你有一个非常大的对象或繁重的构造代码也有其他可访问的静态方法或字段,这些方法或字段可能在需要实例之前使用,然后并且只有那时你才需要使用延迟初始化。

您可以使用private static class来加载实例。代码如下所示:

public final class Foo {
private static class FooLoader {private static final Foo INSTANCE = new Foo();}
private Foo() {if (FooLoader.INSTANCE != null) {throw new IllegalStateException("Already instantiated");}}
public static Foo getInstance() {return FooLoader.INSTANCE;}}

由于行private static final Foo INSTANCE = new Foo();仅在实际使用类Foloader时执行,因此这会处理惰性实例化,并且保证它是线程安全的。

当您还希望能够序列化您的对象时,您需要确保反序列化不会创建副本。

public final class Foo implements Serializable {
private static final long serialVersionUID = 1L;
private static class FooLoader {private static final Foo INSTANCE = new Foo();}
private Foo() {if (FooLoader.INSTANCE != null) {throw new IllegalStateException("Already instantiated");}}
public static Foo getInstance() {return FooLoader.INSTANCE;}
@SuppressWarnings("unused")private Foo readResolve() {return FooLoader.INSTANCE;}}

方法readResolve()将确保返回唯一的实例,即使对象在程序的前一次运行中被序列化。

有时一个简单的“static Foo foo = new Foo();”是不够的。想想你想做的一些基本数据插入。

另一方面,您必须同步任何实例化单例变量的方法。同步本身并不坏,但它可能导致性能问题或锁定(在使用此示例的非常罕见的情况下)。解决方案是

public class Singleton {
private static Singleton instance = null;
static {instance = new Singleton();// do some of your instantiation stuff here}
private Singleton() {if(instance!=null) {throw new ErrorYouWant("Singleton double-instantiation, should never happen!");}}
public static getSingleton() {return instance;}
}

现在会发生什么?类通过类加载器加载。直接从字节数组解释类后,VM执行静态{}-块。这是整个秘密:静态块只调用一次,即给定包的给定类(名称)被这个类加载器加载的时间。

我使用Spring框架来管理我的单例。

它不强制类的“单例性”(如果涉及多个类加载器,您无论如何都无法真正做到这一点),但它提供了一种非常简单的方法来构建和配置不同的工厂以创建不同类型的对象。

如果您需要延迟加载类的实例变量,您需要复核习惯用法。如果您需要延迟加载静态变量或单例,您需要按需方初始化习惯用法。

此外,如果单例需要可序列化,所有其他字段都需要是瞬态的,并且需要实现readResolve()方法来保持单例对象不变。否则,每次反序列化对象时,都会创建一个新的对象实例。readResolve()所做的是替换readObject()读取的新对象,这强制该新对象被垃圾收集,因为没有变量引用它。

public static final INSTANCE == ....private Object readResolve() {return INSTANCE; // Original singleton instance.}

我会说一个枚举单例。

在Java中使用枚举通常是声明枚举单例的一种方式。枚举单例可能包含实例变量和实例方法。为了简单起见,还要注意,如果您使用任何实例方法,那么如果该方法影响对象的状态,则需要确保该方法的线程安全。

枚举的使用非常容易实现,并且对于必须以其他方式规避的可序列化对象没有缺点。

/*** Singleton pattern example using a Java Enum*/public enum Singleton {INSTANCE;public void execute (String arg) {// Perform operation here}}

您可以通过Singleton.INSTANCE访问它,这比在Singleton上调用getInstance()方法要容易得多。

1.12枚举常量的序列化

枚举常量的序列化方式与普通可序列化或可外部化对象不同。枚举常量的序列化形式仅由其名称组成;形式中不存在常量的字段值。要序列化枚举常量,ObjectOutputStream写入枚举常量的name方法返回的值。要反序列化枚举常量,ObjectInputStream从流中读取常量名称;然后通过调用java.lang.Enum.valueOf方法获得反序列化后的常量,将常量的枚举类型以及接收到的常量名称作为参数传递。与其他可序列化或可外部化对象一样,枚举常量可以作为序列化流中随后出现的反向引用的目标。

枚举常量序列化的过程不能自定义:枚举类型定义的任何特定于类的writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve方法在序列化和反序列化期间都会被忽略。类似地,任何serialPersistentFieldsserialVersionUID字段声明也会被忽略——所有枚举类型都有一个固定的serialVersionUID of0L。记录枚举类型的可序列化字段和数据是不必要的,因为发送的数据类型没有变化。

引自Oracle留档

传统Singleton的另一个问题是,一旦你实现了Serializable接口,它们就不再是单例了,因为readObject()方法总是返回一个新实例,就像Java中的构造函数一样。这可以通过使用readResolve()并通过替换为如下单例来丢弃新创建的实例来避免:

 // readResolve to prevent another instance of Singletonprivate Object readResolve(){return INSTANCE;}

如果您的单例类保持状态,这可能会变得更加复杂,因为您需要使它们成为瞬态,但在枚举单例中,JVM保证序列化。


好阅读

  1. 单例模式
  2. 枚举、单例和反序列化
  3. 双重检查锁定和单例模式

以下是三种不同的方法

  1. 枚举

     /*** Singleton pattern example using Java Enum*/public enum EasySingleton {INSTANCE;}
  2. 双重检查锁定/延迟加载

     /*** Singleton pattern example with Double checked Locking*/public class DoubleCheckedLockingSingleton {private static volatile DoubleCheckedLockingSingleton INSTANCE;
    private DoubleCheckedLockingSingleton() {}
    public static DoubleCheckedLockingSingleton getInstance() {if(INSTANCE == null) {synchronized(DoubleCheckedLockingSingleton.class) {// Double checking Singleton instanceif(INSTANCE == null) {INSTANCE = new DoubleCheckedLockingSingleton();}}}return INSTANCE;}}
  3. 静态工厂方法

     /*** Singleton pattern example with static factory method*/
    public class Singleton {// Initialized during class loadingprivate static final Singleton INSTANCE = new Singleton();
    // To prevent creating another instance of 'Singleton'private Singleton() {}
    public static Singleton getSingleton() {return INSTANCE;}}

免责声明:我刚刚总结了所有很棒的答案,并用我自己的话写了下来。


在实现Singleton时,我们有两种选择:

  1. 懒惰加载
  2. 早期装载

延迟加载增加了位开销(说实话很多),所以只有当你有一个非常大的对象或繁重的构造代码还有其他可访问的静态方法或字段可能在需要实例之前使用时才使用它,然后并且只有那时你才需要使用延迟初始化。否则,选择早期加载是一个不错的选择。

实现单例的最简单方法是:

public class Foo {
// It will be our sole heroprivate static final Foo INSTANCE = new Foo();
private Foo() {if (INSTANCE != null) {// SHOUTthrow new IllegalStateException("Already instantiated");}}
public static Foo getInstance() {return INSTANCE;}}

一切都很好,除了它是一个早期加载的单例。让我们试试懒惰加载的单例

class Foo {
// Our now_null_but_going_to_be sole heroprivate static Foo INSTANCE = null;
private Foo() {if (INSTANCE != null) {// SHOUTthrow new IllegalStateException("Already instantiated");}}
public static Foo getInstance() {// Creating only  when required.if (INSTANCE == null) {INSTANCE = new Foo();}return INSTANCE;}}

到目前为止一切顺利,但我们的英雄将无法在与多个邪恶线程单独战斗时生存,他们想要我们英雄的许多实例。所以让我们保护它免受邪恶的多线程:

class Foo {
private static Foo INSTANCE = null;
// TODO Add private shouting constructor
public static Foo getInstance() {// No more tension of threadssynchronized (Foo.class) {if (INSTANCE == null) {INSTANCE = new Foo();}}return INSTANCE;}}

但仅仅保护英雄是不够的,真的!!!这是我们能/应该做的最好的事情来帮助我们的英雄:

class Foo {
// Pay attention to volatileprivate static volatile Foo INSTANCE = null;
// TODO Add private shouting constructor
public static Foo getInstance() {if (INSTANCE == null) { // Check 1synchronized (Foo.class) {if (INSTANCE == null) { // Check 2INSTANCE = new Foo();}}}return INSTANCE;}}

这被称为“双重检查锁定习惯用法”。很容易忘记易失性语句,很难理解为什么它是必要的。详情:"双重检查锁定被破坏"声明

现在我们确定了邪恶的线程,但是残酷的序列化呢?我们必须确保即使在反序列化时也没有创建新对象:

class Foo implements Serializable {
private static final long serialVersionUID = 1L;
private static volatile Foo INSTANCE = null;
// The rest of the things are same as above
// No more fear of serialization@SuppressWarnings("unused")private Object readResolve() {return INSTANCE;}}

方法readResolve()将确保返回唯一的实例,即使对象在我们程序的上一次运行中被序列化。

最后,我们添加了足够的线程和序列化保护,但是我们的代码看起来笨重而丑陋。让我们给我们的英雄一个改造:

public final class Foo implements Serializable {
private static final long serialVersionUID = 1L;
// Wrapped in a inner static class so that loaded only when requiredprivate static class FooLoader {
// And no more fear of threadsprivate static final Foo INSTANCE = new Foo();}
// TODO add private shouting construcor
public static Foo getInstance() {return FooLoader.INSTANCE;}
// Damn you serialization@SuppressWarnings("unused")private Foo readResolve() {return FooLoader.INSTANCE;}}

这就是我们的英雄:)

由于行private static final Foo INSTANCE = new Foo();仅在实际使用类FooLoader时执行,因此这会处理惰性实例化,并且保证它是线程安全的。

我们已经走了这么远。以下是实现我们所做的一切的最佳方式:

public enum Foo {INSTANCE;}

内部将被视为

public class Foo {
// It will be our sole heroprivate static final Foo INSTANCE = new Foo();}

就是这样!不再担心序列化、线程和丑陋的代码。也是ENUMS单例被懒惰初始化

这种方法在功能上等同于公共字段方法,除了它更简洁,提供序列化机制免费,并提供了一个铁定的保证,防止多个实例化,即使面对复杂的序列化或反射攻击。虽然这种方法尚未被广泛采用,单元素枚举类型是实现单例的最佳方式。

约书亚·布洛赫《Java

现在您可能已经意识到为什么ENUMS被认为是实现单例的最佳方式,感谢您的耐心:)

更新到我的博客

对于JSE 5.0及更高版本,采用枚举方法。否则,使用静态单例持有者方法(Bill Pugh描述的一种惰性加载方法)。后一种解决方案也是线程安全的,不需要特殊的语言结构(即易失性或同步)。

最简单的单例类:

public class Singleton {private static Singleton singleInstance = new Singleton();private Singleton() {}public static Singleton getSingleInstance() {return singleInstance;}}
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {if (INSTANCE != null)throw new IllegalStateException(“Already instantiated...”);}

public synchronized static Singleton getInstance() {return INSTANCE;}
}

由于我们已经在getInstance之前添加了同步关键字,因此在两个线程同时调用getInstance的情况下,我们避免了竞争条件。

我仍然认为在Java1.5之后,枚举是可用的最佳单例实现,因为它还确保即使在多线程环境中,也只创建一个实例。

public enum Singleton {INSTANCE;}

你完蛋了!

版本1:

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

延迟加载,线程安全阻塞,由于synchronized而性能低下。

版本2:

public class MySingleton {private MySingleton() {}private static class MySingletonHolder {public final static MySingleton instance = new MySingleton();}public static MySingleton getInstance() {return MySingletonHolder.instance;}}

延迟加载,线程安全,无阻塞,高性能。

看看这篇文章。

Java核心库中的GoF设计模式示例

从最佳答案的“Singleton”部分,

Singleton(可通过每次返回相同实例(通常是自身)的创建方法识别)

  • java.lang.运行时#getRuntime()
  • java.awt.桌面#getDesktop()
  • java.lang.System#getSecurityManager()

您还可以从Java本地类本身学习Singleton的示例。

创建单例对象的各种方法:

  1. 按照Joshua Bloch-枚举是最好的。

  2. 您也可以使用双重检查锁定。

  3. 甚至可以使用内部静态类。

枚举单例

实现线程安全的单例的最简单方法是使用Enum:

public enum SingletonEnum {INSTANCE;public void doSomething(){System.out.println("This is a singleton");}}

此代码自Java1.5中引入枚举以来一直有效

双重检查锁定

如果你想编写一个在多线程环境中工作的“经典”单例(从Java1.5开始),你应该使用这个。

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

这在1.5之前不是线程安全的,因为易失性关键字的实现不同。

早期加载单例(甚至在Java1.5之前工作)

此实现在加载类时实例化单例并提供线程安全。

public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {return instance;}
public void doSomething(){System.out.println("This is a singleton");}
}

这是如何实现一个简单的单例

public class Singleton {// It must be static and final to prevent later modificationprivate static final Singleton INSTANCE = new Singleton();/** The constructor must be private to prevent external instantiation */private Singleton(){}/** The public static method allowing to get the instance */public static Singleton getInstance() {return INSTANCE;}}

这是如何正确地懒惰创建你的单例:

public class Singleton {// The constructor must be private to prevent external instantiationprivate Singleton(){}/** The public static method allowing to get the instance */public static Singleton getInstance() {return SingletonHolder.INSTANCE;}/*** The static inner class responsible for creating your instance only on demand,* because the static fields of a class are only initialized when the class* is explicitly called and a class initialization is synchronized such that only* one thread can perform it, this rule is also applicable to inner static class* So here INSTANCE will be created only when SingletonHolder.INSTANCE* will be called*/private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}}

另一个经常被用来反对单例的论点是它们的可测试性问题。单例不容易被用于测试目的。如果这被证明是一个问题,我想做以下轻微的修改:

public class SingletonImpl {
private static SingletonImpl instance;
public static SingletonImpl getInstance() {if (instance == null) {instance = new SingletonImpl();}return instance;}
public static void setInstance(SingletonImpl impl) {instance = impl;}
public void a() {System.out.println("Default Method");}}

添加的setInstance方法允许在测试期间设置单例类的模型实现:

public class SingletonMock extends SingletonImpl {
@Overridepublic void a() {System.out.println("Mock Method");}
}

这也适用于早期初始化方法:

public class SingletonImpl {
private static final SingletonImpl instance = new SingletonImpl();
private static SingletonImpl alt;
public static void setInstance(SingletonImpl inst) {alt = inst;}
public static SingletonImpl getInstance() {if (alt != null) {return alt;}return instance;}
public void a() {System.out.println("Default Method");}}
public class SingletonMock extends SingletonImpl {
@Overridepublic void a() {System.out.println("Mock Method");}
}

这也有将此功能暴露给普通应用程序的缺点。处理该代码的其他开发人员可能会试图使用“setInstance”方法来更改特定函数,从而更改整个应用程序行为,因此此方法应该在其javadoc中至少包含一个很好的警告。

尽管如此,对于模型测试的可能性(在需要时),这种代码暴露可能是一个可以接受的代价。

有四种方法可以在Java中创建单例。

  1. 急切初始化单例

     public class Test {private static final Test test = new Test();
    private Test() {}
    public static Test getTest() {return test;}}
  2. 延迟初始化单例(线程安全)

     public class Test {private static volatile Test test;
    private Test() {}
    public static Test getTest() {if(test == null) {synchronized(Test.class) {if(test == null) {test = new Test();}}}return test;}}
  3. 带有支架模式的Bill Pugh单例(最好是最好的一个)

     public class Test {
    private Test() {}
    private static class TestHolder {private static final Test test = new Test();}
    public static Test getInstance() {return TestHolder.test;}}
  4. 枚举单例

     public enum MySingleton {INSTANCE;
    private MySingleton() {System.out.println("Here");}}

实现单例有很多细微差别。持有者模式在许多情况下不能使用。在我看来,当使用易失性时——你也应该使用局部变量。让我们从头开始,迭代这个问题。你会明白我的意思的。


第一次尝试可能看起来像这样:

public class MySingleton {
private static MySingleton INSTANCE;
public static MySingleton getInstance() {if (INSTANCE == null) {INSTANCE = new MySingleton();}return INSTANCE;}...}

这里我们有一个MySingleton类,它有一个名为实例的私有静态成员和一个名为getInstance()的公共静态方法。第一次调用getInstance()时,实例成员为空。然后,流将进入创建条件并创建MySingleton类的新实例。随后对getInstance()的调用将发现实例变量已经设置,因此不会创建另一个MySingleton实例。这确保只有一个MySingleton实例在getInstance()的所有调用者之间共享。

但是这个实现有一个问题。多线程应用程序在创建单个实例时会有一个竞争条件。如果多个执行线程同时(或周围)命中getInstance()方法,它们每个都会看到实例成员为空。这将导致每个线程创建一个新的MySingleton实例,随后设置实例成员。


private static MySingleton INSTANCE;
public static synchronized MySingleton getInstance() {if (INSTANCE == null) {INSTANCE = new MySingleton();}return INSTANCE;}

这里我们使用了方法签名中的同步关键字来同步getInstance()方法。这肯定会修复我们的竞争条件。线程现在会阻塞并一次一个地进入方法。但这也会产生性能问题。这个实现不仅同步了单个实例的创建;它同步了对getInstance()的所有调用,包括read。读取不需要同步,因为它们只返回实例的值。由于读取将构成我们调用的大部分(记住,实例化只发生在第一次调用时),同步整个方法会导致不必要的性能下降。


private static MySingleton INSTANCE;
public static MySingleton getInstance() {if (INSTANCE == null) {synchronize(MySingleton.class) {INSTANCE = new MySingleton();}}return INSTANCE;}

这里我们已经将同步从方法签名移动到包装MySingleton实例创建的同步块。但是这解决了我们的问题吗?好吧,我们不再阻止读取,但我们也后退了一步。多个线程将在同一时间或大约同一时间命中getInstance()方法,并且它们都将实例成员视为null。

然后它们将击中同步块,在那里一个人将获得锁并创建实例。当该线程退出块时,其他线程将争夺锁,每个线程将一个接一个地穿过块并创建我们类的新实例。所以我们回到了我们开始的地方。


private static MySingleton INSTANCE;
public static MySingleton getInstance() {if (INSTANCE == null) {synchronized(MySingleton.class) {if (INSTANCE == null) {INSTANCE = createInstance();}}}return INSTANCE;}

在这里,我们从块里面发出另一个检查。如果实例成员已经设置,我们将跳过初始化。这称为双重检查锁定。

这解决了我们的多次实例化问题。但是,我们的解决方案再次提出了另一个挑战。其他线程可能无法“看到”实例成员已更新。这是因为Java如何优化内存操作。

线程将变量的原始值从主存储器复制到CPU的缓存中。然后将值的更改写入该缓存并从该缓存中读取。这是旨在优化性能的Java的一个特性。但这为我们的单例实现带来了问题。第二个线程-由不同的CPU或内核使用不同的缓存处理-将看不到第一个线程所做的更改。这将导致第二个线程将实例成员视为null,从而强制创建我们单例的新实例。


private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {if (INSTANCE == null) {synchronized(MySingleton.class) {if (INSTANCE == null) {INSTANCE = createInstance();}}}return INSTANCE;}

我们通过在实例成员的声明上使用易失性关键字来解决这个问题。这将告诉编译器始终读取和写入主存储器,而不是CPU缓存。

但是这个简单的改变是有代价的。因为我们绕过了CPU缓存,每次对易失性实例成员操作时都会受到性能影响——我们做了四次。我们仔细检查存在性(1和2),设置值(3),然后返回值(4)。有人可能会说这条路径是边缘情况,因为我们只在方法的第一次调用期间创建实例。也许创建时的性能影响是可以容忍的。但即使是我们的主要用例,读取,也会对易失性成员进行两次操作。一次检查存在性,一次返回其值。


private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {MySingleton result = INSTANCE;if (result == null) {synchronized(MySingleton.class) {result = INSTANCE;if (result == null) {INSTANCE = result = createInstance();}}}return result;}

由于性能受到影响是由于直接对易失性成员进行操作,让我们将局部变量设置为易失性的值并对局部变量进行操作。这将减少我们对易失性操作的次数,从而收回一些损失的性能。请注意,当我们进入同步块时,我们必须再次设置局部变量。这确保它与我们等待锁时发生的任何更改保持同步。

我最近写了一篇关于这方面的文章。解构Singleton。您可以在那里找到有关这些示例的更多信息和“持有人”模式的示例。还有一个真实世界的示例展示了双重检查易失性方法。

我见过的最好的单例模式使用供应商界面。

  • 它是通用的可重复使用的
  • 支持延迟初始化
  • 它只在初始化之前进行同步,然后将阻塞供应商替换为非阻塞供应商。

见下图:

public class Singleton<T> implements Supplier<T> {
private boolean initialized;private Supplier<T> singletonSupplier;
public Singleton(T singletonValue) {this.singletonSupplier = () -> singletonValue;}
public Singleton(Supplier<T> supplier) {this.singletonSupplier = () -> {// The initial supplier is temporary; it will be replaced after initializationsynchronized (supplier) {if (!initialized) {T singletonValue = supplier.get();// Now that the singleton value has been initialized,// replace the blocking supplier with a non-blocking suppliersingletonSupplier = () -> singletonValue;initialized = true;}return singletonSupplier.get();}};}
@Overridepublic T get() {return singletonSupplier.get();}}