38

Java的单例模式

 4 years ago
source link: https://www.tuicool.com/articles/ie6vqyJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Java的单例模式(Singleton Pattern)是指在任何情况下,都只有一个类实例存在。该模式也是众多设计模式中最简单的模式之一,但其中还是有不少门道,今天做一个学习总结。

单例模式的多种实现方式

单例模式的实现方式众多,一般的套路就是在常规的类上面增加三个特性:

private

另外,不同的实现方法也有不同的效果,主要从 是否多线程安全 是否惰性初始化 (直到第一次访问时才初始化它的值)、 性能高低 三个方面考量。下面来看各个实现方式。

方式1

public class Singleton1 {
    /**
     * 一个静态的类实例字段
     */
    private static Singleton1 INSTANCE;

    /**
     * 私有的构造方法
     */
    private Singleton1() {
    }

    /**
     * public的工厂方法
     *
     * @return Singleton1
     */
    public Singleton1 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton1();
        }

        return INSTANCE;
    }

    // 省略类的其他一些字段和方法
}

首先说明: 在多线程环境下,这是一个错误的示例 。这种方式在多线程下可能会产生多个实例,除非程序运行于单线程环境下,否则不要使用这种方式。但鉴于有很多其它实现方式,个人觉得如果严格要求单例,压根就不要使用这种方式。

接下来介绍的其它方法都是多线程安全的。另外,为了节省篇幅,代码就不加注释了。

方式2

既然说方式1在多线程环境下可能会产生多个类实例(因为多个线程可能会同时并发调用 getInstance 方法),那这种问题在多线程编程里面我们一般通过加锁的方式解决,也就是这里要说的方式2:

public class Singleton2 {
    private static Singleton2 instance;

    private Singleton2() {
    }

    public static synchronized Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }

        return instance;
    }
}

这种方式功能没问题,但每次调用 getInstance 都会去获取锁,如果调用频率比较高,就会产生性能问题。

方式3

对于方式2可能存在的性能问题,我们可以通过 双重检查锁定模式(DCL,Double-Checked Locking) 优化一下:

public class Singleton3 {
    private static volatile Singleton3 instance;

    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        // 第一次检查
        if (instance == null) {
            synchronized (Singleton3.class) {
                // 第二次检查
                if (instance == null) {
                    instance = new Singleton3();
                }
            }
        }

        return instance;
    }
}

这样性能就会比方式2高很多了,因为第一次检查是没有加锁的,只有第二次检查才加锁了。而除了第一次初始化创建类实例的时候会进入到if代码块内进行加锁之外,其它时候都不会进入二次检查,也就不会加锁了。

另外需要注意的就是我们给实例字段加了 volatile 关键字修饰,主要是为了防止缓存不一致引发的bug(Java的内存模型允许还未完全初始化完全的对象就对外可见)。

虽然DCL的方式解决了性能问题,但它依旧有一些不好的地方:

volatile

那还有没有其它更好的方式呢?当然有,继续往下看。

方式4

public class Singleton4 {
    public static final Singleton4 INSTANCE = new Singleton4();

    private Singleton4() {
    }
}

这种方式直接在声明实例字段的时候就初始化它,而且我们将 INSTANCE 字段设置成public的了,所以就不需要静态工厂方法了。待会与方式5一起讨论。

方式5

public class Singleton5 {
    private static final Singleton5 INSTANCE = new Singleton5();

    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        return INSTANCE;
    }
}

方式4和方式5非常相似,他们各自有各自的优点:

  • 方式4的优点主要有两个:1. 简单。 2. 将实例字段以static final的方式对外暴露,让使用者一看就知道该类是单例的,非常明确。
  • 方式5的优点主要有三个:1. 灵活。比如哪天你改变主意了,不想让该类是单例的了,直接修改 getInstance 方法的内部实现即可,使用者无需感知。2. 我们可以使用泛型实现一个通用的单例工程方法。3. 静态工厂类可以以 方法引用(method reference) 的方式作为supplier。意思就是可以这样写代码:

    Supplier<Singleton5> singleton5Supplier = Singleton5::getInstance;
    Singleton5 instance = singleton5Supplier.get();

那该如何选择呢?《Effective Java 3rd》一书中给出了讨论:对于方式4和方式5,如果上面方式5的优点你用不到,那就 优先 选方式4.

方式3中,我们自己使用锁来保证了单例,而方式4、5其实都是依赖了JVM对于静态字段和代码块依次有序初始化( Java Language Specification 12.4.2 )来解决了多线程并发的问题,代码简单。但这种方式是不是就非常完美了呢?当然不是的!我们在类里面就实例化一个类,这样类在加载的时候就会去初始化,也就是没有达到惰性加载的目的。从刚才给的Java规范处我们知道类的初始化发生在第一次使用该类的字段或者方法,这样我们再优化一下,以达到惰性加载的目的。看方式6.

方式6

public class Singleton6 {
    private static class InstanceHolder {
        private static final Singleton6 INSTANCE = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return InstanceHolder.INSTANCE;
    }
}

这里我们将类实例的创建封装在了内部类 InstanceHolder 中,这样,当我们第一次调用 getInstance 方法时,该类才会初始化。

当然,这种方式也并非完美无缺。其实之前的所有方法都存在序列化的问题:每次反序列化,都会产生一个新的实例。这样就不是单例了。而且只实现Serializable( implements Serializable )接口是不行的,还需要将所有实例字段声明为 transient ,并且提供一个 readResolve 方法:

private Object readResolve() {
    // Return the one true Singleton6 and let the garbage collector
    // take care of the Singleton6 impersonator.
    return InstanceHolder.INSTANCE;
}

原因参见《Effective Java 3rd》Item89.

当然还有另外一种创建单例的方式可以避免序列化这个问题,看方式7.

方式7

public enum EnumSingleton {
    INSTANCE;

    public EnumSingleton getInstance() {
        return INSTANCE;
    }
}

// 使用
// EnumSingleton enumSingleton = EnumSingleton.INSTANCE.getInstance();

这种实现单例的方式借助enum自身的实现机制保证了不会产生刚才说的序列化问题和多线程并发的问题,是目前综合来看最好的一种实现单例的方式。但也存在一些限制,比如无法继承非enum的类,但这种我们可以通过实现接口的方式来绕开。

以上就是常见的创建单例的7种方式。

单例模式存在的问题

单例模式的实质就是某种意义上的全局变量,我们知道在程序中应该尽量避免全局变量的使用,特别是对于可修改的全局变量,否则程序既难维护,也容易滋生各种Bug。有两种方式能改善这种情况:

  • 如果方法的确需要一个单例对象,能否通过参数传递的方式解决?这样一方面是灵活,比如可以传递不同的单例对象;另一方面容易通过Mock来做单元测试。
  • 如果我们真的需要实现单例(比如A类的单例),那可以额外提供一个工厂类(比如B类),然后由该工厂类B来保证A类的单例性,而不是A类自身。这样A类就是一个普通的类了。

另外,还有一些点可能需要注意:

  1. 单例指的是在一个JVM里面保证唯一,这样对于一些分布式系统,或者某些内部采用了分布式技术的系统来说可能会产生问题。
  2. 不同的class loader可能加载不同版本的单例。
  3. 如果没有对象引用该单例了,可能会被GC回收。等下次使用被再次创建的时候,可能已经和上次不完全一样了。

References:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK