Java设计模式——单例模式

单例模式应该是日常开发中用得最多的设计模式了,它的思想就是保证在应用中一个类的实例只能有一个。

什么情形下需要用到单例模式?

在程序中我们经常会遇到有类似配置文件的需求,一般在整个应用中配置信息应该都是需要共享同一份的,这时可以利用单例模式,保证在程序中用到此配置类的实例时,都是同一个实例,保证程序运行的正确。
类似于这种情形还有很多,比如数据库,线程池等。针对这种共享的情形有人可能就会有疑问,那我把这些配置信息都设置为静态的成员变量不就得了,当然,这样做理论上没问题,也能保证程序中共享一份信息,但是静态变量的生命周期是跟随类的,比普通成员变量要长,所以对内存是很大的消耗,这也是单例模式最大的优点,能节省内存。

单例模式的几种写法

饿汉式:

public class SingleInstance {

    private static SingleInstance sInstance = new SingleInstance();

    private SingleInstance(){}

    public static SingleInstance getInstance(){
        return sInstance;
    }
}

可以看出这种写法比较暴力,在类加载的时候就初始化创建了一个实例,不管你需不需要使用都给你创建好了,所以称为饿汉式。这种写法的缺点就是不是懒加载,就算你不需要他也创建了,所以造成了一定的内存浪费。这种方式是线程安全的,可以正常使用,但不推荐

懒汉式1(线程不安全)

public class SingleInstance {

    private static SingleInstance sInstance;

    private SingleInstance(){}

    public static SingleInstance getInstance(){
        if(sInstance==null){
            sInstance = new SingleInstance();
        }
        return sInstance;
    }
}

这种方式是当你需要这个实例调用getInstance()的时候才会去创建,所以称为懒汉式。这种写法看起来是解决了饿汉式浪费内存的情况,但这种写法是线程不安全的

假设有一个线程调用了getInstance() 方法,判断sInstance等于null之后让出了cpu的执行权,此时另外一个线程拿到了cpu的执行权,而且也进入getInstance() 方法后判断sInstance==null也还是true,最终这两个线程就会创建出两个实例,是线程不安全的,在多线程中不可以使用。

既然存在线程不安全问题,那么就加个锁试试

懒汉式2(效率低)

public class SingleInstance {

    private static SingleInstance sInstance;

    private SingleInstance(){}

    public static synchronized SingleInstance getInstance(){
        if(sInstance==null){
            sInstance = new SingleInstance();
        }
        return sInstance;
    }
}

相对于前一种,在getInstance() 方法上加了一个锁,用来保证线程安全,但是这中写法效率太低。

假设线程A拿到了锁,进入了getInstance() 方法,但是方法还没执行完就让出了cpu执行权,此时另外一个线程也需要调用getInstance(),发现锁还没释放,于是就无法继续执行,得等线程A释放锁。但实际上,我们只需要保证在创建实例的时候线程安全就可以了,创建好之后判断sInstance==null为false,直接return就行,不会存在线程安全的问题,所以这种写法降低了程序执行的效率,不推荐使用。

既然这种效率低,是由于锁的范围太大,那换一个锁的位置试试

懒汉式3(线程不安全)

public class SingleInstance {

    private static SingleInstance sInstance;

    private SingleInstance(){}

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

这种写法在实例已经创建好的情况下,会直接返回对象,不用等待锁,提高了运行效率。但是仍然存在线程不安全问题,当实例还没创建的情况下,同懒汉式1一样,当多个线程都判断if(sInstance==null)为true的情况,都会进入锁住的代码块内,最终创建出多个实例。因此这种写法也不可以使用

懒汉式4(双重校验锁DCL,推荐)

public class SingleInstance {简洁

    private static volatile SingleInstance sInstance;

    private SingleInstance(){}

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

分析懒汉式3,是因为当多个线程都能进入同步代码块时,会创建多个对象。所以改为在进入同步代码块之后,再判断一次,这样即使都先后进入了同步代码块,也不会多创建实例了,这样既解决了线程安全的问题,也解决了效率低的问题。这种双重校验的写法应该是平时用的最多的一种。

在双重校验锁方式中,还有个重要的地方做了改动,在声明sInstance时加了volatile关键字,之所以加入这个关键字是因为sInstance = new SingleInstance()这一行代码,不是原子操作。
在jvm中,这一行代码并不是一个操作完成的,而是大概会分成三个步骤去执行

  1. 给sInstance分配内存
  2. new操作,创建对象
  3. 将对象指向内存空间

其中第三步执行之后sInstance就不等于null了,由于还存在指令重排优化,可能会先执行第3步,再执行第2部,假设某个线程先执行了第3步,还没执行第二步,让出了cpu的执行权,此时另外一个线程判断sIntance已经不为null,则直接返回sIntance,但实际上sInstance还并没有创建真正的对象,最终就会导致程序出错。

总结一下这个问题的根源就是某个线程对sInstance的写操作还没完成,存在一个中间状态,此时让另外一个线程拿去进行了读操作,导致出现问题。

解决这个问题的办法就是使用volatile关键字,volatile会禁止指令重排,会保证在上述的三个操作执行完之后,才会让另外一个线程进行读操作,保障sInstance不会出现中间状态被读而产生错误的情况。

使用内部类

public class SingleInstance {

    private SingleInstance(){}

    private static class SingleInstanceHolder{
        private static SingleInstance sInstance = new SingleInstance();
    }

    public static SingleInstance getInstance(){
        return SingleInstanceHolder.sInstance;
    }
}

这种写法个人感觉就比较高级了,它主要利用了内部类的加载时机以及ClassLoader的同步机制保证单例的实现。

这种写法看起来类似于饿汉式的写法,但其实内部类要在外部类调用getInstance()方法使用到它时才会去加载,于是就变成了懒加载模式。其次ClassLoader在加载类的时候会保证只有一个线程来加载,也不会出现线程不安全的问题

使用枚举

public enum SingleInstance {

    sInstance;

}

这种方式写起来就相当暴力了,使用也是直接用SingleInstance.sInstance就可以。

根据枚举的特性,因为sInstance是SingleInstance的一个实例,而且只定义了一个sInstance,sInstance也不能被克隆,所以就保证了单例,同时因为创建枚举的过程是线程安全的,所以在多线程中使用也没有问题

另外枚举自身已经处理了序列化的问题,不会因为反序列化和反射产生多个实例的情况(这一块说实话还不是很了解原理,感觉也不是几句话能说清楚的,所以等深入研究后再做补充)

总结一下,在日常开发中用的比较多的应该就是双重校验锁的方式了,但是现在大牛们更推荐的是静态内部类和枚举的方式,特别是枚举,代码简洁,还能解决反序列化和反射引起的问题

以上就是对单例设计模式的一些理解和总结,如有不对的地方欢迎批评指正

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

推荐阅读更多精彩内容