1.简介

在本文中,我们将讨论在Java中实现单例(Singletons)的两种常见的方法。

2.基于类的单例

最常见的方法是通过创建常规类并确保其具有以下功能来实现Singleton:

  • 私有构造函数
  • 包含其唯一实例的静态字段
  • 用于获取实例的静态工厂方法

我们还将添加一个info属性,为之后测试使用。

因此,我们的实现将如下所示:

public class ClassSingleton {
    private static ClassSingleton INSTANCE;
    private String info = "Initial class info";

    private ClassSingleton(){
    }

    public static ClassSingleton getInstance(){
        if(INSTANCE == null){
            INSTANCE = new ClassSingleton();
        }

        return INSTANCE;
    }

    // getters and setters

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }
}

虽然这是一种常见方法,但必须注意,在多线程方案中这可能会出现问题

简而言之,它可能导致一个以上的实例,从而破坏了模式的核心原理。

尽管有针对此问题的锁定解决方案,但我们的下一种方法从根本上解决了这些问题。

3.基于枚举的单例

如果使用枚举实现单例将会很简单:

public enum EnumSingleton {
    INSTANCE("Initial enum info");

    private String info;

    private EnumSingleton(String info) {
        this.info = info;
    }

    public EnumSingleton getInstance(){
        return INSTANCE;
    }

    //getters and setters

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }
}

此方法具有枚举实现本身所保证的序列化和线程安全性,从而确保内部仅单个实例可用,从而纠正了基于类的实现中指出的问题。

4.用法

要使用我们的ClassSingleton,我们只需要静态获取实例:

    @Test
    public  void test_ClassSingleton(){
        //ClassSingleton object1 = new ClassSingleton(); // ClassSingleton()的构造函数不可见
        ClassSingleton classSingleton1 = ClassSingleton.getInstance();
        System.out.println(classSingleton1.getInfo()); //Initial class info

        ClassSingleton classSingleton2 = ClassSingleton.getInstance();
        classSingleton2.setInfo("New class info");

        assertEquals(classSingleton1, classSingleton2);

        assertEquals(classSingleton1.getInfo(), classSingleton2.getInfo());
    }

至于EnumSingleton,我们可以像使用其他任何Java Enum一样使用它:

    @Test
    public  void test_EnumSingleton(){
        EnumSingleton enumSingleton1 = EnumSingleton.INSTANCE.getInstance();

        EnumSingleton enumSingleton2 = EnumSingleton.INSTANCE.getInstance();
        enumSingleton2.setInfo("New enum info");

        assertEquals(enumSingleton1, enumSingleton2);
        assertEquals(enumSingleton1.getInfo(), enumSingleton2.getInfo());
    }

5.常见陷阱

Singleton是一种看似简单的设计模式,几乎没有程序员在创建Singleton时可能犯的常见错误。

我们用单例区分两种类型的问题:

  • 存在(我们真的需要一个单例吗?)
  • 实现(我们正确实现了吗?)

5.1 存在问题

从概念上讲,单例是一种全局变量。总的来说,我们知道应该避免使用全局变量,特别是如果它们的状态是可变的。

我们并不是说我们永远不要使用单例。但是,我们说可能会有更有效的方法来组织我们的代码。

如果方法的实现依赖于单例对象,为什么不将其作为参数传递呢? 在这种情况下,我们明确显示该方法所依赖的对象。因此,我们可以在执行测试时轻松模拟这些依赖关系(如果需要)。

例如,单例通常用于包含应用程序的配置数据(比如与存储库的连接配置)。如果将它们用作全局对象,则很难为测试环境选择配置。

因此,当我们运行测试时,生产数据库会被测试数据破坏,这是很难接受的。

如果需要单例,则可以考虑将其实例化委派给另一类(一种工厂)的可能性,该类应确保只有一个单例实例在起作用。

5.2 实现问题

即使单例看起来很简单,但其实现也可能会遇到各种问题。所有这些都会导致这样的事实,即我们最终可能会拥有不止一个类的实例。

5.2.1 同步

上面我们介绍的使用私有构造函数的实现不是线程安全的:它在单线程环境中运行良好,但是在多线程环境中,我们应该使用同步技术来保证操作的原子性:

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

注意关键字 同步方法声明。方法的主体具有多个操作(比较,实例化和返回)。

在没有同步的情况下,两个线程可能以这样的方式交织其执行:表达式INSTANCE == null对于两个线程的求值都为true ,会创建两个ClassSingleton实例。

同步 可能会严重影响性能。如果经常调用此代码,应使用诸如延迟初始化双重检查锁定之类的各种技术来加快它的速度(请注意,由于编译器的优化,此方法可能无法按预期工作)。

5.2.2 多个实例

与JVM本身相关的单例还有其他几个问题,可能导致我们最终遇到一个单例的多个实例。这些问题非常微妙,我们将对每个问题进行简要说明:

  1. 每个JVM应该具有唯一性。对于分布式系统或内部结构基于分布式技术的系统来说,这可能是个问题。
  2. 每个类加载器都可能加载其单例版本。
  3. 一旦没有人持有单例,可能会对其进行垃圾回收。此问题不会一次导致多个单例实例的存在,但是在重新创建实例时,该实例可能与其以前的版本有所不同。

6. 总结

在本文中,我们重点介绍如何仅使用Java来实现单例模式,以及如何确保其一致性以及如何使用这些实现。

与往常一样,可以在GitHub上获得代码示例。