深入解析单例模式的七种实现

什么是单例模式

什么是单例模式呢? 我们引用一下维基百科:

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。还有就是我们经常使用的servlet就是单例多线程的。使用单例能够节省很多内存。

如何实现单例模式呢?

我们引用一下维基百科:

实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

好了,我们知道了单例模式的定义和如何使用单例的描述,接下来,就引用Linux Torvalds 的话:

Talk is cheap. Show me the code

让我们来看看单例模式的7种实现方式

单例模式的七种实现

第一种:懒汉式加载

懒汉式加载:最简单的单例模式:2步,1.把自己的构造方法设置为私有的,不让别人访问你的实例,2.提供一个static方法给别人获取你的实例.

懒汉式加载版本单例模式

我们可以看到,这是一个简单的获取单例的一个类,首先我们定义一个静态实例 single, 如何将构造方法变成私有的。并且给外界一个静态获取实例的方法。如果对象不是null,就直接返回实例,从而保证实例。也可以保证不浪费内存。这是我们的第一个实现单例模式的例子。很简单。但是有问题,我们后面再讲。

第二种:饿汉式加载

饿汉式加载版本单例模式

我们看到第二种单例模式,代码量比第一个少了很多,而为什么叫饿汉式呢?我们看代码,我们定义了一个静态的final的实例,并且直接new了一个对象,这样就会导致Single2 类在加载字节码到虚拟机的时候就会实例化这个实例,当你调用getInstance方法的时候,就会直接返回,不必做任何判断,这样做的好处是代码量明显减少了,坏处是,在你没有使用该单例的时候,该单例却被加载了,如果该单例很大的话,将会浪费很多的内存。

我们停下来思考一下

我们如何选择这两种实现方式呢?如果你的项目对性能没有要求,那么请直接使用饿汉式方法实现单例模式,既简单又方便。但是,大部分程序员都是有追求的,岂能不追求性能。那么我们看第一种方式,就是懒汉式,我们刚刚说过,懒汉式既保证了单例,又保证了性能。但是,他真的能保证单例吗?可以确定的是:在单线程模式下,毫无问题,但在复杂的多线程模式下,会怎么样呢?show me code .

测试用例:我们测试一下
测试用例

我们分析一下上面的代码,首先,我们验证的是什么呢?我们想验证多线程下获取懒汉式单例会不会出现错误。也就是出现一个以上的单例,我们如何做呢?首先我们定义一个Set对实例进行去重,然后创建1000个线程(Windows每个进程最多1000个线程,Linux每个进程最多2000个线程),每个线程都去获取实例,并添加到set中,实际上,我们应该使用Collections.synchronizedSet(set)获取一个线程安全的set,但是,这里为了方便,就直接使用HashSet了,然后main线程等待10秒,让1000个线程尽量都执行完毕。最后循环打印set的内容。在某些情况下,会出现2个实例,注意,是某些情况下,一定要多测试几次。下面是我们测试的结果:


测试结果

我们停下来思考一下:

我们通过测试用例发现:高并发情况下,我们的懒加载确实存在bug。为什么会这样呢?我们假设第一个线程进入getInstance方法,判断实例为null,准备进入if块内执行实例化,这时线程突然让出时间片,第二个线程也进入方法,判断实例也为null,并且进入if块执行实例化,第一个线程唤醒也进入if块进行实例化。这时就会出现2个实例。所以出现了bug。So, 我们想要性能(避免上面说的消耗不需要的内存),又要线程安全。那我们该怎么办呢?有点经验的同学心里肯定有数了。show me code.

第三种方式:synchronized 同步式

59{(V}0%M`G546FRI`F4(_9.png

这是我们的第三种方式,我们分析一下代码,我们可以看到,我们仅仅是在第一种懒汉式中加入了一个关键字,synchronized, 使用synchronized保证线程同步,保证同时只有一个进程进入此方法。从而保证并发安全。但是这样做完美吗?我们思考一下我们的代码:我们使用synchronized关键字,相当于每个想要进入该方法的获取实例的线程都要阻塞排队,我们仔细思考一下:需要吗?当实例已经初始化之后,我们还需要做同步控制吗?这对性能的影响是巨大的。是的,我们只需要在实例第一次初始化的时候同步就足够了。我们继续优化。

第四种方式:双重检验锁:

双重检验锁

我们继续分析一下代码:首先看getInstance方法,我们在方法声明上去除了synchronized关键字,多线程进入方法内部,判断是否为null,如果为null,多个线程同时进入if块内,此时,我们是用Single4 Class对象同步一段方法。保证只有一个线程进入该方法。并且判断是否为null,如果为null,就进行初始化。我们想象一下,如果第一个线程进入进入同步块,发现该实例为null,于是进入if块实例化,第二个线程进入同步内则发现实例已经不是null,直接就返回 了,从而保证了并发安全。那么这个和第三种方式又什么区别呢?第三种方式的缺陷是:每个线程每次进入该方法都需要被同步,成本巨大。而第四种方式呢?每个线程最多只有在第一次的时候才会进入同步块,也就是说,只要实例被初始化了,那么之后进入该方法的线程就不必进入同步块了。就解决并发下线程安全和性能的平衡。虽然第一次还是会被阻塞。但相比较于第三种,已经好多了。

我们还对一个东西感兴趣,就是修饰变量的volatile关键字,为什么要用volatile关键字呢?这是个有趣的问题。我们好好分析一下:
首先我们看,Java虚拟机初始化一个对象都干了些什么?总的来说,3件事情:

  1. 在堆空间分配内存
  2. 执行构造方法进行初始化
  3. 将对象指向内存中分配的内存空间,也就是地址

但是由于当我们编译的时候,编译器在生成汇编代码的时候会对流程进行优化(这里涉及到happen-before原则和Java内存模型和CPU流水线执行的知识,就不展开讲了),优化的结果式有可能式123顺序执行,也有可能式132执行,但是,如果是按照132的顺序执行,走到第三步(还没到第二步)的时候,这时突然另一个线程来访问,走到if(single4 == null)块,会发现single4已经不是null了,就直接返回了,但是此时对象还没有完成初始化,如果另一个线程对实例的某些需要初始化的参数进行操作,就有可能报错。使用volatile关键字,能够告诉编译器不要对代码进行重排序的优化。就不会出现这种问题了。

我们看到,小小的单例模式被我们弄得很复杂。但这就是一个程序员的追求,追求最好的性能,追求最好的代码。

那还有没有别的更好的办法呢?这个代码也太多了,代码可读性也不好。而且线程第一次进入还会阻塞,还能更完美吗?

第五种方式:既要懒汉式加载,又要线程安全:静态内部类。

我们来分析一下代码:相比较饿汉式(也就是第二种),我们增加了一个内部类,内部类中有一个外部类的实例,并且已经初始化了。我们回忆一下饿汉式有什么问题,饿汉式的问题是:在你没有使用该单例的时候,该单例却被加载了,如果该单例很大的话,将会浪费很多的内存.但是,我们现在引入了内部类的方式,虚拟机的机制是,如果你没有访问一个类,那么是不会载入该类进入虚拟机的。当我们使用外部类的时候其他属性的时候,是不会浪费内存载入内部类中的单例的。从而也就保证了并发安全和防止内存浪费。
但是,这样就能完美了吗?

第六种方式:反射和反序列化破坏单例

我们知道Java的反射几乎是什么事情都能做,管你什么私有的公有的。都能破坏。我们是没有还手之力的。精心编写的代码就被破坏了,而反序列化也很厉害,但是稍微还有点办法遏制。什么办法呢?重写readResolve方法。show me code。

我们看到:我们重写了readResolve方法,在该方法中直接返回了我们的内部类实例。重写readResolve() 方法,防止反序列化破坏单例机制,这是因为:反序列化的机制在反序列化的时候,会判断如果实现了serializable或者externalizable接口的类中包含readResolve方法的话,会直接调用readResolve方法来获取实例。这样我们就制止了反序列化破坏我们的单例模式。那反射呢?我们有办法吗?

第七种方式:最后一招,使用枚举

为什么使用枚举可以呢?枚举类型反编译之后可以看到实际上是一个继承自Enum的类。所以本质还是一个类。 因为枚举的特点,你只会有一个实例。我们看一下反编译的枚举类。


反编译的class字节码

我们看到,我们的hello包下的Single7枚举继承了java.lang.Enum<> 类。事实上就是一个类,但是我们这样就能防止反射破坏我们辛苦写的单例模式了。因为枚举的特点,而他也能保证单例。堪称完美!!!

总结

回到开始,我们引用了一些维基百科的话,我们再看看维基百科关于并发是怎么说的:

单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率).

我们看到维基百科还是靠谱的。告诉了我们可以使用互斥锁来防止并发出现的问题。

而单例模式带来了什么好处呢?

  1. 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
  2. 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

小小的一个单例模式也是如此的复杂,耗费了我们很多的精力去写,去读一篇文章。可是,这就是我们的乐趣。任何一段小小的代码,我们都要精益求精。总有一天,我们会写出千万人用的优良代码。加油!!

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

推荐阅读更多精彩内容

  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    成热了阅读 4,231评论 4 34
  • 1 场景问题# 1.1 读取配置文件的内容## 考虑这样一个应用,读取配置文件的内容。 很多应用项目,都有与应用相...
    七寸知架构阅读 6,674评论 12 68
  • 1.单例模式概述 (1)引言 单例模式是应用最广的模式之一,也是23种设计模式中最基本的一个。本文旨在总结通过Ja...
    曹丰斌阅读 2,888评论 6 47
  • 泸江城中流 七星湖水幽 滇越传灵气 青山何来愁
    石竹阅读 264评论 13 4