从未这么明白的设计模式(一):单例模式

96
jsbintask B67c298d f020 4f89 aac6 0710bc0709ec
1.0 2019.01.29 16:47* 字数 1773
cover

什么是单例?为什么要用单例?

一个类被设计出来,就代表它表示具有某种行为(方法),属性(成员变量),而一般情况下,当我们想使用这个类时,会使用new关键字,这时候jvm会帮我们构造一个该类的实例。而我们知道,对于new这个关键字以及该实例,相对而言是比较耗费资源的。所以如果我们能够想办法在jvm启动时就new好,或者在某一次实例new好以后,以后不再需要这样的动作,就能够节省很多资源了。

哪些类可以使用单例?

一般而言,我们总是希望无状态的类能够设计成单例,那这个无状态代表什么呢? 简单而言,对于同一个实例,如果多个线程同时使用,并且不使用额外的线程同步手段,不会出现线程同步的问题,我们就可以认为是无状态的,再简单点:一个类没有成员变量,或者它的成员变量也是无状态的,我们就可以考虑设计成单例。

实现方法

好了,我们已经知道什么是单例,为什么要使用单例了,那我们接下来继续讨论下怎么实现单例。
一般来说,我们可以把单例分为行为上的单例管理上的单例行为上的单例代表不管如何操作(此处不谈cloneable,反射),至始至终jvm中都只有一个类的实例,而管理上的单例则可以理解为:不管谁去使用这个类,都要守一定的规矩,比方说,我们使用某个类,只能从指定的地方’去拿‘,这样拿到就是同一个类了。
而对于管理上的单例,相信大家最为熟悉的就是spring了,spring将所有的类放到一个容器中,以后使用该类都从该容器去取,这样就保证了单例。
所以这里我们剩下的就是接着来谈谈如何实现行为上的单例了。一般来说,这种单例实现有两种思路,私有构造器,枚举

枚举实现单例

枚举实现单例是最为推荐的一种方法,因为就算通过序列化,反射等也没办法破坏单例性,例子:

public enum SingletonEnum {
    INSTANCE;

    public static void main(String[] args) {
        System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
    }
}

结果自然是true,而如果我们尝试使用反射破坏单例性:

public enum BadSingletonEnum {
    /**
     *
     */
    INSTANCE;

    public static void main(String[] args) throws Exception{
        System.out.println(BadSingletonEnum.INSTANCE == BadSingletonEnum.INSTANCE);

        Constructor<BadSingletonEnum> badSingletonEnumConstructor = BadSingletonEnum.class.getDeclaredConstructor();
        badSingletonEnumConstructor.setAccessible(true);
        BadSingletonEnum badSingletonEnum = badSingletonEnumConstructor.newInstance();

        System.out.println(BadSingletonEnum.INSTANCE == badSingletonEnum);
    }
}

结果如下:

Exception in thread "main" java.lang.NoSuchMethodException: cn.jsbintask.BadSingletonEnum.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at cn.jsbintask.BadSingletonEnum.main(BadSingletonEnum.java:18)

异常居然是没有init方法,这是为什么呢? 那我们反编译查看下这个枚举类的字节码:

// class version 52.0 (52)
// access flags 0x4031
// signature Ljava/lang/Enum<Lcn/jsbintask/BadSingletonEnum;>;
// declaration: cn/jsbintask/BadSingletonEnum extends java.lang.Enum<cn.jsbintask.BadSingletonEnum>
public final enum cn/jsbintask/BadSingletonEnum extends java/lang/Enum {

  // compiled from: BadSingletonEnum.java

  // access flags 0x4019
  public final static enum Lcn/jsbintask/BadSingletonEnum; INSTANCE

  // access flags 0x101A
  private final static synthetic [Lcn/jsbintask/BadSingletonEnum; $VALUES
}

结果发现这个枚举类继承了抽象类java.lang.Enum,我们接着看下Enum,发现构造器:

/**
    * Sole constructor.  Programmers cannot invoke this constructor.
    * It is for use by code emitted by the compiler in response to
    * enum type declarations.
    *
    * @param name - The name of this enum constant, which is the identifier
    *               used to declare it.
    * @param ordinal - The ordinal of this enumeration constant (its position
    *         in the enum declaration, where the initial constant is assigned
    *         an ordinal of zero).
*/
protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

那我们接着改变代码,反射调用这个构造器:

public enum BadSingletonEnum {
    /**
     *
     */
    INSTANCE();

    public static void main(String[] args) throws Exception{
        System.out.println(BadSingletonEnum.INSTANCE == BadSingletonEnum.INSTANCE);

        Constructor<BadSingletonEnum> badSingletonEnumConstructor = BadSingletonEnum.class.getDeclaredConstructor(String.class, int.class);
        badSingletonEnumConstructor.setAccessible(true);
        BadSingletonEnum badSingletonEnum = badSingletonEnumConstructor.newInstance("test", 0);

        System.out.println(BadSingletonEnum.INSTANCE == badSingletonEnum);
    }
}

结果如下:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at cn.jsbintask.BadSingletonEnum.main(BadSingletonEnum.java:21)

这次虽然方法找到了,但是直接给我们了一句Cannot reflectively create enum objects,不能够反射创造枚举对象,接着我们继续看下newInstance(...)这个方法:

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

关键代码就是:if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");,所以就是jdk从根本上拒绝了使用反射去创建(知道为啥java推荐使用enum实现单例了吧),另外,我们再观察下Enum类的clone和序列化方法,如下:

protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

private void readObject(ObjectInputStream in) throws IOException,
    ClassNotFoundException {
    throw new InvalidObjectException("can't deserialize enum");
}

private void readObjectNoData() throws ObjectStreamException {
    throw new InvalidObjectException("can't deserialize enum");
}

一眼看出,直接丢出异常,不允许这么做!(真亲儿子系列)
所以,结论就是:枚举是最靠谱的实现单例的方式!

私有构造器

另外一个实现单例最普通的方法则是私有构造器,开放获取实例公共方法,虽然这种方法还是可以用clone,序列化,反射破坏单例性(除非特殊情况,我们不会这么做),但是却是最容易理解使用的。而这种方式又分了饱汉式饿汉式

饿汉式

看名字就知道,饥渴!(咳咳,开个玩笑),它指的是当一个类被jvm加载的时候就会被实例化,这样可以从根本上解决多个线程的同步问题,例子如下:

public class FullSingleton {
    private static FullSingleton ourInstance = new FullSingleton();

    public static FullSingleton getInstance() {
        return ourInstance;
    }

    private FullSingleton() {
    }

    public static void main(String[] args) {
        System.out.println(FullSingleton.getInstance() == FullSingleton.getInstance());
    }
}

结果自然是true,虽然这种做法很方便的帮我们解决了多线程实例化的问题,但是缺点也很明显,因为这句代码private static FullSingleton ourInstance = new FullSingleton();的关系,所以该类一旦被jvm加载就会马上实例化,那如果我们不想用这个类怎么办呢? 是不是就浪费了呢?既然这样,我们来看下替代方案! 饱汉式。

饱汉式

既然是,就代表它不着急,那我们可以这么写:

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

用意很容易理解,就是用到getInstance()方法才去检查instance,如果为null,就new一个,这样就不怕浪费了,但是这个时候问题就来了:现在有这么一种情况,在有两个线程同时 运行到了 instane == null这个语句,并且都通过了,那他们就会都实例化一个对象,这样就又不是单例了。既然这样,哪有什么解决办法呢? 锁方法

  1. 直接同步方法
    这种方法比较干脆利落,那就是直接在getInstance()方法上加锁,这样就解决了线程问题:
public class HungrySafeSingleton {
    private static HungrySafeSingleton instance;

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

        return instance;
    }

    private HungrySafeSingleton() {
        System.out.println("HungryUnsafeSingleton.HungryUnsafeSingleton");
    }

    public static void main(String[] args) {
        System.out.println(HungrySafeSingleton.getInstance() == HungrySafeSingleton.getInstance());
    }
}

很简单,很容易理解,加锁,只有一个线程能实例该对象。但是,此时问题又来了,我们知道对于静态方法而言,synchronized关键字会锁住整个 Class,这时候又会有性能问题了(尼玛墨迹),那有没有优化的办法呢? 双重检查锁

public class HungrySafeSingleton {
    private static volatile HungrySafeSingleton instance;

    public static HungrySafeSingleton getInstance() {
        /* 使用一个本地变量可以提高性能 */
        HungrySafeSingleton result = instance;

        if (result == null) {

            synchronized (HungrySafeSingleton.class) {

                result = instance;
                if (result == null) {
                    instance = result = new HungrySafeSingleton();
                }
            }
        }

        return result;
    }

    private HungrySafeSingleton() {
        System.out.println("HungryUnsafeSingleton.HungryUnsafeSingleton");
    }

    public static void main(String[] args) {
        System.out.println(HungrySafeSingleton.getInstance() == HungrySafeSingleton.getInstance());
    }
}

用意也很明显,synchronized关键字只加在了关键的地方,并且通过本地变量提高了性能(effective java),这样线程安全并且不浪费资源的单例就完成了。

总结

本章,我们一步一步从什么是单例,到为什么要使用单例,再到怎么使用单例,并且从源码角度分析了为什么枚举是最适合的实现方式,然后接着讲解了饱汉式,饿汉式的写法以及好处,缺点。
例子源码:https://github.com/jsbintask22/design-pattern-learning.git
本文原创地址:https://jsbintask.cn/2019/01/29/designpattern/singleton/,转载请注明出处。
如果你觉得有用,欢迎关注,分享!

设计模式