单例模式

单例模式在Android中算是很常用的一类模式了,当我们需要整个软件中有且只有一个实例对象时,我们可以写一个单例类。

最简单的单例

public class RecycleBin {
    private static RecycleBin INSTANCE;
    
    private RecycleBin() { }
    
    public static RecycleBin getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new RecycleBin();
        }
        return INSTANCE;
    }

这样的代码大家肯定都不陌生,如果INSTANCE还没有实例化,那么实例化它,并且构造方法是private的,防止客户端new一个实例。这样的代码在单线程环境下可以较好的工作,但是在多线程环境下就会出现错误。假如线程A执行if (INSTANCE == null)这一步后被挂起,线程B切换进来执行了这段代码,实例化了INSTANCE,此时INSTANCE就不为null了,之后把CPU重新让给了线程A,但此时A并不知道INSTANCE已被实例化这件事,又将INSTANCE指向了一个新的RecycleBin对象。

同步的单例模式

那么实现多线程下的单例模式呢。一个很粗暴的答案是:synchronized。是的,给getInstance方法加上这个关键字后,整个方法就是同步的了,可以实现单例模式。

public synchronized static RecycleBin getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new RecycleBin();
        }
        return INSTANCE;
    }

但是这么做会产生新的问题:效率很低。如果有多个线程要同时获得该对象,那么线程需要排队,一个一个获取,这会造成很大的浪费。

改进的单例模式

可以换个角度,因为是在检查INSTANCE == null这一步代码上出现了问题,那么将锁加到这个代码段上即可,将代码修改为

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

DCL(Double Check Locking)

因为synchronized的同步会产生大量的性能开销,追求性能的大佬发明了双重锁的办法来减小开销。

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

比起上面的代码,多了一道检测INSTANCE == null的工序,这行代码可以避免除了第一次以后的同步。当对象已经实例化之后,就不会执行同步代码块了。但是这样的代码还是会存在问题,因为INSTANCE = new RecycleBin()这一行代码不是原子操作
下面来假设一个出错的场景

  1. 线程A执行了getInstance方法
  1. 线程A检查INSTANCE变量,发现为空
  2. 线程A执行INSTANCE == new RecycleBin(),将INSTANCE设置为了非空,但是在构造方法执行前被挂起了
  3. 线程B执行代码,检查INSTANCE变量,发现不为空,返回INSTANCE对象。(但此时INSTANCE还未调用构造方法)

实际上这一行代码被拆成了三步来执行

memory = allocate();            //#1为对象分配内存空间
init(memory);                   //#2初始化
instance = memory;              //#3设置instance,将其指向刚分配的内存空间。

在某些编译器上,2和3会出现倒序,也就是类的域无法得到初始化,从而拿到一个并不正确的对象。
庆幸的是再Java1.5之后的版本可以给INSTANCE加上volitate关键字来避免编译器的优化,拿到正确的对象。

饿汉式

上面的几个方法都是属于懒汉式的方法,即等需要了再去实例化。接下来介绍另一种叫做饿汉式:在类加载时就初始化对象

public class RecycleBin {
    private static RecycleBin INSTANCE = new RecycleBin();
    
    private RecycleBin() { }
    
    public static RecycleBin getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new RecycleBin();
        }
        return INSTANCE;
    }

因为静态域只会加载一次,所以产生的单例对象是安全的。

内部类

public class Singleton {
    // 获得对象实例的方法
    public static Singleton getSingleton() {
        return SingletonHolder.instance;
    }

    /**
     * 静态内部类与外部类的实例没有绑定关系,而且只有被调用时才会
     * 加载,从而实现了延迟加载
     */
    private static class SingletonHolder {
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static Singleton instance = new Singleton();
    }

    private Singleton() {
    }
}

枚举

public enum Singleton {
    // 定义枚举元素,他就是Singleton的一个实例
    INSTANCE;

    public void doSomething() {
        // do something
    }
}

使用枚举可以说是最佳的单例实践方式,因为即便构造器是私有的,仍然可以通过反射来调用私有构造器如

public class TestMain {
    public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Class<?> classType = Singleton.class;  
        Constructor<?> c = classType.getDeclaredConstructor(null);  
        c.setAccessible(true);  
        Singleton singleton1 = (Singleton) c.newInstance();  
        Singleton singleton2 = Singleton.getSingleton();  
        System.out.println(singleton1 == singleton2);  
    }
}

另外还有一种特殊情况是反序列化,反序列化并不是通过调用构造器来构造对象的,反序列化操作提供了readSolve方法来重建对象,如果我们要避免反序列化时产生新的对象需要复写这个方法

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

而枚举帮我们完成了这些工作

小结

这部分也只是看书看了个大概,对多线程环境下单例的各种坑并不了解,这部分同时涉及了Java中类的加载机制,多线程,堆,栈等概念。DCL的错误的那部分也看的不是很懂。。这样一个简单的设计模式都有这么多花样,还要学习一个啊。

参考资料

Android设计模式解析与实战
单例模式各版本的原理与实践
Java线程安全兼谈DCL

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

推荐阅读更多精彩内容