设计模式之单例模式

单例模式

单例模式定义

  • 确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

单例模式是“创建型”设计模式之一。

单例模式的使用场景

  • 确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对
    象只应该有且只有一个。例如,创建一个对象需要消耗的资源过多,如要访问I0和数据库等资源,这时就要考虑使用单例模式。

单例模式UML类图

单例模式UML.png

角色介绍:

  • Client高层客户端;
  • Singleton单例类。

实现单例模式主要有如下几个关键点:

  1. 构造函数不对外开放,一般为Private;
  2. 通过一个静态方法或者枚举返回单例类对象;
  3. 确保单例类的对象有且只有一个,尤其是在多线程环境下;
  4. 确保单例类对象在反序列化时不会重新构建对象。

通过将单例类的构造函数私有化,使得客户端代码不能通过new的形式手动构造单例类的对象。单例类会暴露一个公有静态方法,客户端需要调用这个静态方法获取到单例类的唯一对象,在获取这个单例对象的过程中需要确保线程安全,即在多线程环境下构造单例类的对象也是有且只有一个,这也是单例模式实现中比较困难的地方。

单例类的实现

1. 饿汉模式

饿汉模式 - java版本

public class SimpleSingleton {
    private static SimpleSingleton mInstance = new SimpleSingleton();

    private SimpleSingleton() {
    }

    public static SimpleSingleton getInstance() {
        return mInstance;
    }
}

饿汉模式 - kotlin版本

object SimpleSingleton {
    fun dosomething() {
        println("dosomething")
    }
}

饿汉式优缺点

优点:

  • 在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题

缺点:

  • 如果构造方法中存在过多的处理,会导致加载这个类时比较慢,可能引起性能问题。
  • 如果使用饿汉式的话,只进行了类的装载,并没有实质的调用,会造成资源的浪费。

适用场景:

  • 这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。

2. 懒汉模式

懒汉模式 - java版本

public class LazySingleton {
    private static LazySingleton mInstance = null;

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (null == mInstance) {
            mInstance = new LazySingleton();
        }
        return mInstance;
    }
}

懒汉模式 - kotlin版本

class LazySingleton {
    companion object {
        val mInstance: LazySingleton by lazy { LazySingleton() }
    }
}

说明:

  • 显式声明构造方法为private
  • companion object用来在class内部声明一个对象
  • LazySingleton的实例instance 通过lazy来实现懒汉式加载
  • lazy默认情况下是线程安全的,这就可以避免多个线程同时访问生成多个实例的问题

懒汉式优缺点

优点:

  • 懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。

缺点:

  • java版本的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题(getInstance()方法上加上synchronized字段)。

适用场景:

  • 如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。

3. 双重校验锁

双重校验锁版本

public class DoubleCheckSingleton {
    private static volatile DoubleCheckSingleton mInstance = null;

    private DoubleCheckSingleton() {
    }

    public static DoubleCheckSingleton getInstance() {
        if (mInstance == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (mInstance == null) {
                    // Double checked
                    mInstance = new DoubleCheckSingleton();
                }
            }
        }
        return mInstance;
    }
}
  • 双重校验锁解决了“懒汉式”单例模式的线程安全问题。
  • 增加了 volatile 关键字,禁止指令重排序优化。

4. 静态内部类

静态内部类版本

public class StaticInnerSingleton {
    private static class SingletonHolder {
        static StaticInnerSingleton mInstance = new StaticInnerSingleton();
    }

    private StaticInnerSingleton() {
    }

    public static StaticInnerSingleton getInstance() {
        return SingletonHolder.mInstance;
    }
}
  • 这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。
  • 不一样的是,它是在内部类里面去创建对象实例。
  • 这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。

5. 枚举单例

public class EnumSingleton {
    private EnumSingleton() {
    }

    public static EnumSingleton getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;
        private EnumSingleton singleton;

        //JVM会保证此方法绝对只调用一次
        private Singleton() {
            singleton = new EnumSingleton();
        }

        public EnumSingleton getInstance() {
            return singleton;
        }
    }
}
  • 在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法。
  • 同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时(Singleton.INSTANCE),我们的单例被实例化。
  • 也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。

总结:

上面提到的五种种实现单例的方式都有共同的缺点:

  • 需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
  • 可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。

单例模式的线程安全性:

  • 饿汉式:线程安全
  • 懒汉式:非线程安全
  • 双检锁:线程安全
  • 静态内部类:线程安全
  • 枚举:线程安全

参考链接:
https://blog.csdn.net/fly910905/article/details/79286680
https://droidyue.com/blog/2017/07/17/singleton-in-kotlin/

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