设计模式之单例模式

单例模式是设计模式中相对是来说最容易理解的设计模式,但其实大多数人只是知道单例模式的一些简单场景的使用和实现。比如饿汉单例、懒汉单例等。实际上,在很多场景下,这些实现方式有很多问题。那么在不同场景下我们应该如何选择单例的实现呢。这次我们会由浅入深的学习,彻底的来认识一下单例模式。

一、单例模式的定义

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

二、UML类图

它的类图很简单,就是client使用这个单例类就行了。
需要注意的是单例类的构造方法是私有的。

单例UML.png

三、使用场景

它的使用场景也很明确。
确保某个类只要一个对象的场景,从而避免产生多个对象消耗过多的资源。

从定义、UML类图和使用场景都透露出两个字--简单。

可它的背后真的只是那么简单吗?哈哈,表明简单的东西背后往往是不简单的。

嗯哼,不信,那就先放一张图来震下场子

单例模式.png

这就是我这篇文章要说的核心内容了。

四、不同场景下的实现

1.说实现之前,我们先要看看实现单例模式的关键点

(1)构造方法不对外开放。这个很容易理解,如果开放了那么和话,外面随便new一下就实现了一个对象,还单例个毛线呢。
(2)确保单例类的对象只有一个,尤其是在多线程环境下。这个后面会详细介绍。
(3)确保单例类的对象在反序列化时不会重新构建对象。

那么下面我一一介绍各种实现方法和它们的优缺点

2.最简单的实现方法--饿汉单例

饿汉,顾名思义,非常饥饿的汉子,所以一定义这个对象就给实例化了。

public class HungerSingleton {

    private static HungerSingleton singleton=new HungerSingleton();
    private HungerSingleton() {}
    
    public static HungerSingleton getSingleton() {
        return singleton;
    }
}

实现非常简单,通过一个静态方法返回一个静态对象。

但是缺点也很明显,如果我不需要这个对象的话,它也会创建对象,浪费空间资源和影响时间性能。

3.给饥饿的汉子一张床--懒汉单例

给了饥饿的汉子一张床后会忘记饥饿,但它会变懒。也就不急着实例化对象了,就是所谓的懒加载。

public class LazySingleton {

    private LazySingleton(){};
    
    private static LazySingleton singleton=null;
    
    public static  synchronized LazySingleton getSingleton() {
        if(singleton==null){
            singleton=new LazySingleton();
        }
        return singleton;
    }
}

仔细一看我们发现这个getSingleton方法加了个synchronize关键字。这个就是我们之前说的”保证在多线程下对象唯一“的方法。

咋一看很完美,但还是有一个很大的问题就是:

我们知道synchronize同步是比较消耗性能的,而且每次调用getSingleton方法都会进行同步,这回造成不必要的性能开销。

4.懒汉单例的优化--DCL单例

针对上面的懒汉单例的缺点,出现了DCL(Double Check Lock)模式。其实解决的方式很简单了,只需要在外层加一个判空就OK了。

这样就整个程序就有了两次判空,第一次的目的是为了避免多余的同步操作。第二次是为了对象实例化的唯一性。

public class DCLSingleton {

    private DCLSingleton(){};

    private static DCLSingleton singleton=null;
    
    private static DCLSingleton getSingleton(){
        if (singleton==null) {
            synchronized (DCLSingleton.class) {
                if(singleton==null)
                    singleton=new DCLSingleton();
            }
        }
        return singleton;
    }
}

这里我们考虑到了多线程的情景又考虑到性能的问题。看起来就完美了。其实不深入研究下去,这的确是完美的单例写法。那么还有什么不完美的地方呢?

就是这句话

singleton=new DCLSingleton();

这个语句他不是一个原子操作。

也就是说

它表明上只有一步其实有三步操作

(1)给DCLSingleton的实例分配内存

(2)调用Singleton()的构造方法,初始化成员字段

(3)将singleton对象指向分配的内存空间

由于java编译器允许处理器乱序执行,以及jdk1.5之前的java内存模型(JMM)中寄存器、cache到主内存回写顺序的规定。所以不能保证上面三步执行的顺序。很有可能会按照(1)-(3)-(2)的顺序
这就会有一个问题,高并发的情况下,当A线程执行到(3),被切换到了B线程,那么B线程就会直接取走了singleton对象,但是该对象因为没有执行第二步,就没有真正的实例化。所以就会被B拿去使用导致出错,发生DCL失效。

那肿么办呀?这个我们没法控制啊。

幸运的是,在jdk1.5的时候,出现了一个volatile关键字。当我们的jdk版本大于等于1.5的时候,只要在定义的时候加上这个volatile就能够解决了问题了。

private static volatile DCLSingleton singleton=null;

虽然有点小瑕疵但是DCL的优点还是多的,大多数情景下可以保证单例对象的唯一性。但如果你的并发场景复杂或者版本低于jdk1.5的时候,可能需要一个更好的实现方式了。

5.《java并发编程实践》推荐--静态内部类单例

java并发编程实践书中并不赞同DCL实现方式,因为这种方式还是会出现DCL失效。它建议使用静态内部类实现

public class InternalClassSingleton {
    private InternalClassSingleton(){};
    
    public static InternalClassSingleton getSingleton(){
        return SingletonHolder.singleton;
    }
    
    private static class SingletonHolder{
        private static final InternalClassSingleton singleton=new InternalClassSingleton();
    }

}

这种方式也是很简洁的,它只会在第一次调用getSingleton()的时候才会让singleton被初始化。因此第一次加载getSingleton的时候会导致虚拟机加载SingletonHolder类,这种方式不仅能够保证单例对象的唯一性,同时也延迟了单例的实例化。推荐使用的单例模式实现方式。

6.最简单的单例实现--枚举单例

我们都知道枚举和普通的java类是一样的,不仅有字段还有自己的方法。重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它只有一个单例。

public class EnumSingleton {
    private EnumSingleton(){};
    enum Enum{
        INSTANCE;
        public static Enum getInstance(){
            return INSTANCE;
        }
    }
}

实在是简单,看着还舒服,重点不需要考虑多线程的场景问题。美滋滋~~

除了上面说到,其实还有其他的方式实现,比如利用容器等等。但上面的方式足以应对各种的场景了,我们只需要根据不同的场景和自己的喜好来选择合适的就好了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容