一篇文章详解单例模式

0. 序言

  • 我们要熟练掌握单例模式。不管是实战开发中,还是面试手写设计模式中,都少不了它。
  • 通过阅读本篇博文,你会了解常用的单例模式,单例模式三要素,以及如何保证单例模式的安全性。

1. 定义

  • 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

2. UML类图

单例模式UML.png

3. 通用代码(饿汉式)

public class Singleton {

    private static final Singleton sSingleton = new Singleton();

    //限制产生多个对象
    private Singleton() {
    }

    //通过该方法获得实例对象
    public static Singleton getSingleton() {
        return sSingleton;
    }

    //类中其他方法,尽量是static
    public static void doSometing() {

    }
}

4. 三要素(非常重要)

  • 私有构造函数。
  • 暴露公有静态方法返回单例类唯一对象。
  • 在多线程环境下确保单例类对象有且只有一个。

5. 优点

  • 只有一个实例,减少内存开支。(频繁创建时)
  • 只生成一个实例,减少系统性能开销。(一个对象需要比较多的资源时)
  • 避免对资源的多重占用,以免对同一个资源文件同时操作。(比如:写文件时)
  • 优化和共享资源访问。(可以在系统设置全局的访问点)

6. 缺点

  • 没有接口,扩展困难,只能修改代码。疑问:为什么不增加接口呢?因为单例模式要求“自行实例化”,接口对单例模式没有意义。
  • 对测试不利。并行开发环境中,单例模式没有完成,是不能进行测试的。
  • 单例模式与单一职责原则有冲突。后者规定一个类应该只实现一个逻辑,是不是单例取决于环境。前者规定必须是单例而且没有规定只能有一个逻辑。

7. 使用场景

  • 当要求一个类有且只有一个对象,出现多个对象就会发生“不良反应”时,比如访问I/O或者数据库等资源。
  • 整个项目需要一个共享访问点或者共享数据。
  • 工具类对象。

8. 注意事项(一)

在高并发的情况下,饿汉式不会出现产生多个实例的情况,但是懒汉式就要注意线程的同步问题,懒汉式代码如下:

public class Singleton {

    private static Singleton sSingleton = null;

    //限制产生多个对象
    private Singleton() {
    }

    //通过该方法获得实例对象
    public static Singleton getSingleton() {
        if (sSingleton == null) {
            sSingleton = new Singleton();
        }
        return sSingleton;
    }
    
}

该单例模式在低并发的情况下并不会出现问题,若并发量增大则可能出现多个实例!为什么会这样呢?

如一个线程A执行到sSingleton = new Singleton(),但是没有获得对象(对象初始化是需要时间的),第二个线程B也在执行,执行到(sSingleton == null)判断,那么线程B获得判断条件也是真,于是继续运行下去,线程A获得了一个对象,线程B也获得了一个对象,在内存中就出现两个对象。

解决线程不安全的方法有很多,可以在getSingleton方法前加sychronized关键字,也可以在getSingleton方法内增加sychronized来实现。

public class Singleton {

    private static Singleton sSingleton = null;

    //限制产生多个对象
    private Singleton() {
    }

    //通过该方法获得实例对象
    public static sychronized Singleton getSingleton() {
        if (sSingleton == null) {
            sSingleton = new Singleton();
        }
        return sSingleton;
    }
}

优点是单例只有在使用时才会被实例化,在一定程序上节约了资源;缺点是第一次加载时需要及时进行实例化,反应稍慢,最大的问题是每次调用getInstance都进行同步,造成不必要的同步开销。

所以相比懒汉式,更加推荐饿汉式,当然各有利弊,下文会推荐几种适用的单例模式,别着急,接着往下看。

9. 注意事项(二)

除了担心高并发以外,还需要考虑对象的复制情况。

在Java中,对象默认是不可以被复制的,若实现Cloneable接口,实现clone方法,则可以直接通过对象复制方法创建一个新对象,对象复制是不用调用类的构造函数的,因此即使是私有的构造函数,对象仍然可以被复制。所以解决该问题的方法就是单例类不要实现Cloneable接口。

10. 推荐写法

  • 饿汉式模式
public class Singleton {

    private static final Singleton sSingleton = new Singleton();

    //限制产生多个对象
    private Singleton() {
    }

    //通过该方法获得实例对象
    public static Singleton getSingleton() {
        return sSingleton;
    }

    //类中其他方法,尽量是static
    public static void doSometing() {

    }
}

优点:类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。这种方式基于类加载机制,避免了多线程的同步问题。

缺点:不能达到懒加载的效果,如果从始至终未使用过这个实例,则会造成内存的浪费。

推荐场景:单例模式经常使用的场景下,选择饿汉式。

  • 双重检索模式(DCL)
public class Singleton {

    private static Singleton sSingleton = null;

    //限制产生多个对象
    private Singleton() {
    }

    //通过该方法获得实例对象
    public static Singleton getSingleton() {
        if (sSingleton == null) {
            synchronized (Singleton.class) {
                if (sSingleton == null) {
                    sSingleton = new Singleton();
                }
            }

        }
        return sSingleton;
    }
    
}

亮点:在getSingleton方法中对instance进行了两次判空:第一层判断主要是为了避免不必要的同步,第二层判断则是为了在Singleton等于null的情况下才创建实例。

分析:
sSingleton = new Singleton()这句话大致做了3件事情:
①:给Singleton的实例分配内存。
②:调用Singleton()构造函数,初始化成员字段。
③:将sSingleton对象指向分配的内存空间(此时sSingleton不是null了)
但是由于Java编译器允许处理器乱序执行等原因,执行顺序可能是1,2,3,还可能是1,3,2.如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候sSingleton因为已经在线程A内执行过了第三点,sSingleton已经是非空了,所以线程B直接取走sSingleton,再使用时就会出错,导致DCL失效。
为了避免这类事情的发生,JDK1.5之后调整了JVM,具体化了volatile关键字,只需要将sSingleton定义改成private volatile static Singleton sSingleton = null 就可以保证sSingleton对象每次都是从主内存中读取的,保证了可见性和有序性,这样的话就可以使用DCL。以后会有相关Java内存方面的文章,具体阐述volatile关键字。

优点:资源利用率高,第一次执行getSingleton时单例对象才会被实例化。
缺点:第一次加载时反应稍慢。

推荐理由:资源利用率高!线程安全!绝大多数场景下可以保证单例对象的唯一性。

完整代码:

public class Singleton {

    private volatile static Singleton sSingleton = null;

    //限制产生多个对象
    private Singleton() {
    }

    //通过该方法获得实例对象
    public static Singleton getSingleton() {
        if (sSingleton == null) {
            synchronized (Singleton.class) {
                if (sSingleton == null) {
                    sSingleton = new Singleton();
                }
            }

        }
        return sSingleton;
    }
    
}
  • 静态内部类单例模式(最推荐的)

DCL虽然解决了资源消耗、多余的同步、线程安全等问题,但是在高并发场景比较复杂的情况下依然会出现失效的问题,所以推荐使用静态内部类单例模式:

public class Singleton {

    //限制产生多个对象
    private Singleton() {
    }

    //通过该方法获得实例对象
    public static Singleton getInstance() {
        return SingletonHolder.sInstance;
    }

    /**
     * 静态内部类
     */
    private static class SingletonHolder {
        private static final Singleton sInstance = new Singleton();
    }

}

优点:

  1. 懒加载:
    第一次加载Singleton类时并不会初始化sInstance,只有在第一次调用Singleton的getInstance方法时才会导致sInstance被初始化。
  2. 线程安全和单例对象唯一性:
    第一次调用Singleton的getInstance方法时会导致虚拟机加载SingletonHolder类,这种方式确保了线程安全,还保证了单例对象的唯一性。

推荐理由:懒加载、线程安全、单例对象唯一性。

  • 枚举模式
public enum Singleton {

    INSTANCE;

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

    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

优点:默认枚举实例的创建实线程安全的,并且在任何情况下它都是一个单例,包括序列化。

推荐理由:写法简单,甚至在反序列化的情况下,依然可以保证单例的唯一性。

  • 容器模式
public class Singleton {

    private static Map<String, Object> objMap = new HashMap<String, Object>();

    private Singleton() {
    }

    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return objMap.get(key);
    }

}

分析:在程序的初始,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

11. 特别注意

通过反序列化,上面几种单例模式都会出现
重新创建对象的情况,枚举不包括在内。

通过反序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而获得一个实例。及时构造函数是私有的,反序列化依然可以通过特殊途径去创建类的一个新的实例。

反序列化操作提供了一个很特别的函数,类中具有一个私有的、被实例化的方法readResolve,通过这个方法可以让开发人员控制对象的反序列化。

所以上述几个示例(不包括枚举)中如果要杜绝单例对象在被反序列化时重新生成对象,必须加入以下方法

private Object readResolve() throws ObjectStreamException {
        return sInstance;
}

readResolve方法将sInstance对象返回,而不是默认的重新生成一个新的对象。

12. 后续

如果大家喜欢这篇文章,欢迎点赞;如果想看更多 设计模式 方面的技术,欢迎关注!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容