设计模式:单例模式 的四种创建方式和一些思考

单例模式的含义:

程序运行过程中,希望某个实体(对象)在内存中只有一个实例。

使用条件:

  • 该实例的创建比较消耗资源;
  • 该实例有单一性,比如某个 Fragment,不希望每次使用时就重新创建。

比如使用 Glide 加载图片,获取 Glide 对象的过程就是一个单例模式。因为 Glide 对象的创建过程相当复杂,而且在使用的过程中只需要一个可用 Glide 对象。

public static Glide get(Context context) {
    if(glide == null) {
        Class var1 = Glide.class;
        synchronized(Glide.class) {
            if(glide == null) {
                Context applicationContext = context.getApplicationContext();
                List modules = (new ManifestParser(applicationContext)).parse();
                GlideBuilder builder = new GlideBuilder(applicationContext);
                ...
                glide = builder.createGlide();
                ...
            }
        }
    }
    return glide;
}

单例模式的创建:

一、饿汉式


一般来说创建对象首先想到构造函数,那么单例的创建可用先从某对象的构造函数说起。

  1. 首先来说因为该类的单例对象只创建一次,可用考虑使用 static 修饰,这样在 JVM 加载该类的时候就会自动创建对象;
  2. 接下来我们不希望其他类执行该单例类的构造方法再去创建单例对象,所以把构造函数的属性设置为 private;(感叹 Java 的封装设计)
  3. 那么存在单例对象就需要把这个对象暴露出去,通过 public 的方法无疑是一种比较好的方式。
    这里为什么不直接把单例设置为 public 呢?这里就联系到设计原则:迪米特原则了,其他类对该单例类了解的尽量少。其他类获取该单例类的对象只需要通过其方法暴露出来即可,而不需要了解单例具体是怎么创建的。假如该单例类创建的过程变得更加复杂,其他类的调用还是通过这个简单的方法获得对象而不用关心单例类增加了哪些代码。
public class Singleton {

    // 私有单例变量,在 JVM 加载时就会自动创建
    private static Singleton INSTANCE = new Singleton();

    // 创建私有构造方法,避免其他类再创建对象
    private Singleton(){
    }
    
    // 向其他类暴露获取实例的方法
    public static Singleton newInstance(){
        return INSTANCE;
    } 

}

那么这种创建方式就是 饿汉式,这种创建方式特点:

  • 优点:创建简单、线程安全:因为依赖 JVM 的类加载机制,执行类初始化的时候 JVM 会获取一个锁,可以同步多个线程对一个类完成初始化
  • 缺点:不适合创建复杂,占用内存大的单例对象
  • 应用场景:初始化简单、创建速度快

二、懒汉式


1. 懒汉式(普通使用)

或许我们需要更加灵活地创建和销毁某个单例对象,又或许需要使用的单例对象比较复杂占用内存比较多,所以需要一种灵活的按需创建的方式:

  1. 与之前的逻辑类似,依旧不想被其他类通过构造方法或直接获取单例对象,所以构造函数和单例对象应该是 private 的;
  2. 需要一个方法把单例对象暴露出去,这个方法中灵活创建单例对象。
public class Singleton {

    // 定义私有变量并设置初始为 null
    private static Singleton INSTANCE = null;

    // 创建私有构造方法,避免其他类再创建对象
    private Singleton(){
    }

    // 向其他类暴露获取实例的方法,需要时再创建
    public static Singleton newInstance(){
        if (INSTANCE == null){
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }

}

这种创建方式就是懒汉式,但是这种方式存在明显的问题,就是线程安全问题:

  • 缺陷:在多线程的情况下调用获取单例对象 INSTANCE,假设线程 A 调用 newInstance() 时单例对象还未创建,执行创建过程。线程 B 也来到这个方法来判断单例对象是否创建,这时由线程 A 申请创建的单例对象还未创建成功,于是又执行一次创建过程。这样在内存中就存在两个 Singleton 对象的实例,单例模式也就失去了意义。
2. 懒汉式(同步锁)

既然这种方式有缺陷而且是线程安全问题,那么就加个同步锁 synchronized 来访问创建单例对象的部分:

public class Singleton {

    // 定义私有变量并设置初始为 null
    private static Singleton INSTANCE = null;

    // 创建私有构造方法,避免其他类再创建对象
    private Singleton(){
    }

    // 加入同步锁,确保同一时刻只有一个线程访问该方法
    public static synchronized Singleton newInstance(){
        if (INSTANCE == null){
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
    
    // *第二种写法
    =========================================================
    public static Singleton newInstance(){
        if (INSTANCE == null){
            synchronized (Singleton.class){
                INSTANCE = new Singleton();
            }
        }
        return INSTANCE;
    }

}
  • 优点:防止多个线程同步访问 newInstance() 反复创建实例
  • 缺点:每次访问都要进行线程同步,造成过多的同步开销(加锁 = 耗时、耗能)
3. 懒汉式(双重校验)
public class Singleton {

    // 使用 volatile 关键字
    private static volatile Singleton INSTANCE = null;

    // 创建私有构造方法,避免其他类再创建对象
    private Singleton(){
    }

    public static Singleton newInstance(){
        // 第一重判断
        if (INSTANCE == null){
            synchronized (Singleton.class){
                // 第二重判断
                if(INSTANCE == null){
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

}
  • 优点:多加入一层判断,一定程度上避免多次进入同步锁方法。volatile 关键字保证线程在使用该变量时,都去堆里拿最新的数据。
  • 缺点:判断略多,容易混淆。

三、静态内部类


利用 Java 静态类的特点来创建单例对象。

public class Singleton {

    private static class SingletonStatic{
        // 在静态内部类中加载外部类静态对象
        private static Singleton INSTANCE = new Singleton();
    }
    // 私有构造器
    private Singleton(){
    }
    // 获取静态内部类实例化的对象
    public static Singleton newInstance(){
        return SingletonStatic.INSTANCE;
    }
}

上面代码可以看出:

  1. 使用静态内部类来创建外部类静态对;
  2. 使用 newInstance() 时才会加载静态内部类,创建单例对象并返回;
  3. 该静态内部类只会被 JVM 加载一次,利用这个特性解决线程同步问题。

四、枚举方式创建单例


public enum  Singleton {

    INSTANCE;

    // 隐藏默认的空的私有构造方法
    // private Singleton(){}
}

// 使用
Singleton singleton = Singleton.INSTANCE;

这种创建方式利用枚举的特性保证了 按需加载、线程同步

参考资料:

单例模式(Singleton) - 最易懂的设计模式解析
从Android代码中来记忆23种设计模式

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

推荐阅读更多精彩内容