从一生所爱到单例模式

概要

好无聊,无聊到只能用学习来打发时间了。

刚刚听音乐,正好放到了一首我非常喜欢的歌 一生所爱。就是大话西游里面的那首。每每听到这首歌,心,痛。

咦,好像一生所爱和我今天要总结的单例模式关联不上了,不管了,下面直接开始总结,不然我的心就白白痛了啵。


饿汉式

个人以为饿汉式是实现单例最简单粗暴等方式了。无论是谁,随便拿一本 Java 书籍,只要看过前 30 页,就能写饿汉式的单例。原因很简单只要知道静态变量在类加载的时候就被创建了,那么任意定义静态变量并直接初始化,你不想单例都难了。

class SingletonHungry {
    private static SingletonHungry instance = new SingletonHungry();

    private SingletonHungry() {}

    public static SingletonHungry getInstance() {
        return instance;
    }
}

饿汉式的单例实现起来十分简单而且还是线程安全的,但是并没有延时初始化,这会对内存造成浪费,就像很多安卓手机应用,明明没用到,却在开机时候就在后台偷偷启动了。不过鉴于如今硬件升级速度之快,可以让我们不用那么关心这点内存问题。

懒汉式

懒汉式分两种,一种是线程不安全的,还有一种是线程安全的。

如下这段个单例在多线程环境下就有可能会出错。

//懒汉式 线程不安全
class SingletonLazy {
    private static SingletonLazy instance;

    private SingletonLazy() {}

    public static SingletonLazy getInstance() {
        if (null == instance) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

多线程出错演示


接下来是线程安全的懒汉式

//懒汉式 线程安全
class SingletonLazySafe {
    private static SingletonLazySafe instance;

    private SingletonLazySafe() {}

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

看线程安全和不安全的两种实现方式代码其实没什么差别,唯一不同的地方就是线程安全的方式加了一个关键字 synchronized,用于确保 getInstance 方法线程安全。但是这样线程安全是安全了,那么如果接下来有上百上千个线程来访问这个方法,每个线程调用一次就把这个方法先锁一次,你说你死不死。所以这两种其实都不是实现单例的好方法。

双重校验模式 double-checked locking

说到底 双重校验模式 也是 懒汉式 的一种。它解决了上面懒汉式效率低下的问题。

class SingletonDCL {
    private static SingletonDCL instance;

    private SingletonDCL() {}

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

可以从代码中看出,我们学聪明了,没有将整个 getInstance 方法给锁住,而是只锁创建实例的部分。另外进行了两次 null 判断。

这两次判断还是比较讲究的,第一次判断是否为 null 的意图很明显,就是为了如果实例已经存在那么我们就直接返回实例,不要再做任何多余的操作。

第二次判断是为了解决多线程情况下创建多个实例的问题。(假设一下没有这个判断,当两个线程 A,B 同时访问这个方法,A 线程先获取了锁,B 线程在等待。A 线程完成实例化操作,释放了锁,然后 B 线程开始执行实例化操作,这个时候因为没有判断 instance 已经被实例化过了,又重新实例化一遍,这就不是单例了。)

所以,双重校验模式才是真正的懒汉式,嗯!

登记式/静态内部类

使用静态内部类方式能达到双重校验模式一样的效果,而且实现更加简单。

这里用到了 Java 静态内部类的特性,加载类的时候,不会加载静态内部类,只有在使用的时候才会进行显示装载。

class SingletonRegister {
    private static class SingletonInner {
        private static final SingletonRegister INSTANCE = new SingletonRegister();
    }

    private SingletonRegister() {

    public static SingletonRegister getInstance() {
        return SingletonInner.INSTANCE;
    }
}

枚举方式

说实话,从没有在实际项目中看到用枚举方式实现单例的,不过这是 Effective Java 作者 Josh Bloch 提倡的方式。

这种方式实现起来简单的让人疑惑 这是单例吗?没错,还真是。

enum SingletonEnum {
    INSTANCE("url", "name", "pwd", "etc");
    private String url;
    private String name;
    private String pwd;
    private String etc;

    SingletonEnum(String url, String name, String pwd, String etc) {
        this.url = url;
        this.name = name;
        this.pwd = pwd;
        this.etc = etc;
    }

    public void doSomething() {
        System.out.println("do something");
    }

    public SingletonEnum getInstance() {
        return INSTANCE;
    }
}

## 总结
一般在实际项目中使用到单例模式时候,会以饿汉模式作为首选,如果有延时加载要求的那才会用内部类实现方式。如果有反序列化需求的,那么可以用枚举方式实现,毕竟 Enum 已经实现了 Serializable 接口。

总结好了,老朽要再去刷遍《大话西游》了。

Comments
Write a Comment