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


当前回答

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

根据约书亚·布洛赫的说法,埃努姆将是最好的。也可以使用双重检查锁定。甚至可以使用内部静态类。

其他回答

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

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

使用类似Spring framework的DI框架,然后将其作为DI配置的一部分,无论依赖项是单体的、请求范围的、会话范围的还是其他。

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

枚举单例

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

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

自从在Java 1.5中引入Enum以来,此代码一直有效

双重检查锁定

如果你想编写一个在多线程环境中工作的“经典”单例(从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之前不是线程安全的,因为volatile关键字的实现不同。

早期加载单例(甚至在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 modification
    private 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 instantiation
    private 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();
    }
}

我可以说是枚举单例。

在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方法获得反序列化的常量,并将常量的枚举类型与接收到的常量名称作为参数一起传递。与其他可序列化或可外部化对象一样,枚举常量可以用作后续在序列化流中出现的反向引用的目标。无法自定义枚举常量的序列化过程:在序列化和反序列化期间,将忽略枚举类型定义的任何类特定的writeObject、readObject、readObjectNoData、writeReplace和readResolve方法。类似地,任何serialPersistentFields或serialVersionUID字段声明也会被忽略——所有枚举类型都具有固定的serialVersionID 0L。记录枚举类型的可序列化字段和数据是不必要的,因为发送的数据类型没有变化。引自Oracle文档

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

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

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


好的阅读

Singleton模式枚举、Singleton和反序列化双重检查锁定和Singleton模式

实现单例有很多细微差别。夹持器图案不能在许多情况下使用。在使用volatile时,也应该使用局部变量。让我们从头开始,反复讨论这个问题。你会明白我的意思的。


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

public class MySingleton {

     private static MySingleton INSTANCE;

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

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

但这种实现有一个问题。多线程应用程序在创建单个实例时具有竞争条件。如果多个执行线程同时(或前后)命中getInstance()方法,它们将分别将INSTANCE成员视为null。这将导致每个线程创建一个新的MySingleton实例,然后设置instance成员。


private static MySingleton INSTANCE;

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

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


private static MySingleton INSTANCE;

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

在这里,我们将同步从方法签名移到了一个同步块,该块包装MySingleton实例的创建。但这能解决我们的问题吗?嗯,我们不再阻止读取,但我们也后退了一步。多个线程将同时或几乎同时命中getInstance()方法,它们都会将INSTANCE成员视为空。

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


private static MySingleton INSTANCE;

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

在这里,我们从区块内部发出另一张支票。如果INSTANCE成员已经设置,我们将跳过初始化。这称为双重检查锁定。

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

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


private static volatile MySingleton INSTANCE;

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

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

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


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;
}

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

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