我要做 Android 之单例模式

单例模式

  1. 懒汉式(线程不安全)

单例模式最后的目的无非就是获取当前存在的实例对象,如果没有实例对象就实例化一个,有就直接返回。所谓懒汉式,可以从名字进行理解,就是在你第一次使用这个实例之前我都(懒得)不去进行实例化,一直等到第一次需要使用到这个单例的地方再(迫不得已)实例化。

提到单例模式,大多数开发者会直接写出下面的代码:

public class SingleTon {

    private static SingleTon instance;

    private SingleTon(){

    }

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

而上面的代码也就是懒汉式的单例模式,只有在第一次调用了getInstance方法的地方才会对instance进行实例化。

这种单例模式的写法在单线程的时候没有问题,但需要注意的是,在 多线程的情况下,这种单例模式的写法可能会出现实例化多个单例的情况,即这种懒汉模式是线程不安全的,所以一般情况下也是不推荐这么写单例的。

  1. 懒汉式(线程安全)

像多数线程不安全的问题的解决方法相同,实现线程安全的懒汉单例进行加锁即可,例如直接将锁加在上面的例子里获取单例的静态方法getInstance上:

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

显然,这种方法可以解决线程不安全的问题,即使多线程并行也只会存在一个单例,但这种方法的缺点是效率太低,仔细想想,其实有必要进行加锁的只有第一次判断instance为空而进行实例化的时候,而在instance实例化完成后,后面无论是哪个线程读取到的instance都已经不为null,可以直接将当前instance返回。而这种对整个方法进行加锁的操作则会使得每一次获取单例都进行加锁,效率十分低下。

  1. 双重检验锁

针对上面提出的问题,出现了第三种单例模式的写法,即双重检验锁的单例模式:

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

所谓双层检验锁,即像上面的例子中所示的,在加锁前后对实例对象进行两次判空的检验,加锁是为了第一次对象实例化的线程同步,而锁内还要有第二层判空是因为可能会有多个线程进入第一层if判断内部,而在加锁代码块外排队等候,如果锁内不进行第二次检验,仍然会出现实例化多个对象的情况。

而进行了双重检验之后,除了第一次实例化需要进行加锁同步,之后的线程只要进行第一层的if判断不为空即可直接返回,而不用每一次获取单例都加锁同步,因此相比前面两种单例模式的写法,更推荐用这种双层检验锁的方式来写单例。

然而,双层检验锁也不是完美的,问题出现在对单例对象进行实例化的过程中,在执行instance = new SingleTon()这句代码时大概做了三件事:

  1. 给instance分配内存空间
  2. 通过调用SingleTon的构造函数初始化成员变量
  3. 将instance这个引用指向先前分配好的内存空间

JVM在执行编译的时候实际上不能保证上述三件事中2和3的执行顺序,也就是说可能发生的执行顺序是123或132,如果是123的执行顺序则不会发生问题,而如果是132的执行顺序,则instance作为一个引用,在指向分配好的内存空间的时候本身就已经不为null了,此时如果有另一条线程调用getInstance方法,则会直接将还没有对成员变量完成初始化的instance返回,程序就可能发生崩溃。

为了解决这个问题,我们加入了violate关键字:

private static violate SingleTon instance;

violate的作用可以理解为一个读写锁,使用violate修饰后,一旦开始对instance进行操作,则只能将前面所说的三步全部进行完之后才能再读取instance,有了violate关键字,才能解决上面提到的问题。

  1. 饿汉式

可以看到,前面说的所有问题的根源都出在多线程上,如果可以解决多线程同步的问题,那么前面的问题也就迎刃而解了,于是就出现了饿汉式的单例模式。

对比懒汉式,懒汉是懒得进行实例化,直到要用到这个单例的时候才进行实例化,那么饿汉就是饥饿难耐、等不及的就进行了实例化,直接来看代码:

public class SingleTon {

    private static SingleTon instance = new SingleTon();

    private SingleTon(){

    }

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

显然,由于被修饰为static变量,instance会在类第一次加载到内存中时就进行了实例化,由于类加载机制的特性,这种写法本身就是线程安全的,所以不会有前面三种方式中存在的问题。

由于static修饰,instance会在类第一次被加载到内存中时就被初始化,如果某次运行期间并没有用到这个单例,就浪费了资源去加载了这个对象,不能做到延时加载;另一方面,有些时候我们需要依赖其他类创建的资源来创建这个单例,即需要在我们第一次调用getInstance方法的时候才真正的创建单例对象,这种一开始就初始化完成的方式也无法满足这种需求。

  1. 静态内部类式

针对上面饿汉式方法存在的问题,能不能有一种既能延迟加载、又能解决同步问题的单例模式写法呢?这便需要用静态内部类的方式了。

public class SingleTon {

    private static class SingleTonInstance{
        private static SingleTon instance = new SingleTon();
    }

    private SingleTon(){

    }

    public static SingleTon getInstance(){
        return SingleTonInstance.instance;
    }
}

由于静态内部类是私有的,因此除了getInstance方法没有其他方式能访问到它,因此只有在getInstance调用时才能真正创建单例,实现了单例创建的延迟,同时由于静态类只会加载一次的特性,因此也不会出现线程同步问题

  • 都需要额外的工作实现序列化,否则每次反序列化一个序列化对象时都会创建一个新的实例;
  • 仍然可以使用反射强行调用私有的构造函数构建单例。
  1. 枚举

实际上,在Java中最优雅的单例写法是使用枚举。

public enum  EnumSingle {
    INSTANCE;
}

简单的一个枚举,实际上就解决了线程同步、防止反射调用构造函数的问题,另一方面,枚举提供了自动序列化机制,防止了反序列化的时候重新创建对象,因此这是新版effective Java最推荐的单例写法。

注意

前面说枚举是最推荐的单例写法,从理论上来说,同时解决了前面所说的同步问题、反射调用构造函数问题、反序列化重新创建对象问题的枚举写法确实是最优雅的单例模式,但是需要注意的是,枚举类并不是普通类,使用枚举类对象作为单例自然丢弃了一些普通类的特性(继承等),而且枚举和饿汉式写法一样,无法实现延迟加载。

所以综上所述,如果不需要延迟加载,可以优先使用枚举实现单例;如果需要延迟加载,可以优先使用静态内部类。

当然,上面的分析是针对Java来说的,但是实际上在Android中因为性能问题不推荐使用枚举,而且Android中使用的Java都是在1.5版本以上的,所以Android使用的单例推荐使用静态内部类或者双重检验锁。

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

推荐阅读更多精彩内容