本文原文地址:https://xiaoqinyu0000.github.io/2017/01/10/Java/JavaSingleton/
是我在闲暇之余看到的一片文章,看过后觉得很有意思,将单例模式讲的很生动形象,就转载过来,给大家分享一下,感谢大家支持。

1.背景

在Java帝国,有一个隐蔽的村庄叫IO村,村里每个人都身怀绝技。其中,SocketIO、HttpIO、FileIO更是专注于某个领域的高手。

FileIO,它十余年苦练文件存储技术,雄心壮志,决定走出村庄,去外面闯一闯。

FileIO到了城里,成功的通过了面试,进入一家IO科技责任有限公司,专门负责文件存储等工作。

2.懒汉式

FileIO刚进入公司,工作不久,就接到3、4次投诉,投诉理由是“在系统中使用FIleIO之后,频繁地发生内存抖动,导致内存吞吐量骤减”。

FIleIO大急,连忙查看日志,原来在系统运行中,自己的实例被频繁地创建与销毁。

FileIO找到主管老张: “咱们能不能给客户说一下, 让客户对我的实例进行缓存,别这么频繁地创建啊”。

老张:“我们很难去规定客户怎么用,不过我们可以做一些引导。我看你也不需要创建多个实例,你可以创建一个实例给客户使用,不对外开放创建实例的权限就行”。

FileIO眼睛一亮,赶紧请教老张具体该怎么做。

老张说:“其实很容易。

第一、把自己的构造方法设置为private的,不让别人new你的实例;

第二、提供一个static方法给别人获取你的实例,你在这个方法里面返回你自己创建的实例就行”。

FileIO按照老张的思路做了修改:

public class FileIO {private static FileIO fileIO;private FileIO(){}public static FileIO getInstance(){if (fileIO == null){fileIO = new FileIO();}return fileIO;}//... ...
}

以后别人在调用FIleIO的时候就不再使用new的方式去创建一个FIleIO的实例,而是调用static方法getInstance()获取FileIO的实例,例:

FileIO.getInstance().openFile(fileName);

这样一来,FileIO就不会频繁的被创建了。

后来,FileIO才知道自己的这种实现机制就是“单例设计模式”,并且被人称为“懒汉式”, 可能是因为在需要的时候才创建吧, 显得很“懒”。

3.饿汉式

FileIO使用单例模式修改后解决了问题,在公司例会上被技术总监大大地表扬了一次。

FileIO非常傲娇,在村里的微信群里向小伙伴们炫耀这件事。

村里的HttpIO自己也存在类似的问题,也需要自修一下,于是它按照了FileIO在微信群中分享的思路进行修改:

public class HttpIO {private static final HttpIO INSTANCE = new HttpIO();private HttpIO(){}public static HttpIO getInstance(){return INSTANCE;}//... ...
}

实现的方式虽有所不同,但调用方式和效果都是一样的(都能实现单例)。

HttpIO总觉得自己的实现方式更好, FileIO则说自己的Lazy方式更流行, 毕竟不调用FileIO的话对象就不会创建。

两人争执不下, 于是就去请教经验丰富的老张。

老张说,你们这都是单例设计模式的实现方式,HttpIO的实现方式在单例设计模式中被称为饿汉式”(可以能由于太饥饿, 一上来就创建了对象)两者的执行顺序有所不同:

FileIO的实现在第一次调用的时候先执行了getInstance()方法,再执行构造方法。
HttpIO的实现在第一次调用的时候先执行了构造方法,再执行getInstance()方法。
而且,饿汉式是一种线程安全的写法。

4.线程安全(synchronized)

FileIO表示不服, 老张说,我给你举个例子,

当有多个线程并发执行getInstance()的时候,可能会出现以下的情况而导致FileIO产生多个实例。”

线程一 : FileIO.getInstance()

(FileIO:判断fileIO为null,进行fileIO实例的初始化)

线程二: FileIO.getInstance()

(FileIO: fileIO还没初始化完,依然为null, 于是进行另外一个fileIO实例的初始化)

等到两个线程都返回的时候,其实是创建了两个FileIO的实例。

FileIO恍然大悟,线程安全可得好好重视,好在以前研究过一点线程安全的问题,直接加上synchronized:

public class FileIO {private static FileIO fileIO;private FileIO(){}public synchronized static FileIO getInstance(){if (fileIO == null){fileIO = new FileIO();}return fileIO;}//... ...
}

这样一来,当有两个线程同时执行getInstance()方法的时候,一旦线程一获取到FileIO.class锁,线程二只能在外面等待着。

在线程一执行完getInstance()的逻辑后释放FileIO.class锁,其他线程才能获取这个锁进入getInstance()方法中。

这样就避免了创建两个FileIO的实例

流程如下:

5.线程安全-双重检验锁

老张锊一锊胡须,道:“这种做法简单明了,确实保证了线程安全, 还能更优化呢!”

FileIO不服气,“我不信,难道还有比我只加一个synchronized更简单的做法?”。

老张笑了一下,“我说的优化并不是指简单化。你想想啊,如果fileIO实例不为空时,还需要使用synchronized来限制执行时只能一个线程进入吗?来来来,老哥给你露一手。”

public class FileIO {private static volatile FileIO fileIO;private FileIO() {}public static FileIO getInstance() {if (fileIO == null) {synchronized (FileIO.class) {if (fileIO == null) {fileIO = new FileIO();}}}return fileIO;}//... ...
}

“我这招江湖人称”双重检验锁“,够酷吧!”。

“老司机就是复杂,好端端的,被你弄了那么多个fileIO == null的判断,还用volatile关键字修饰fileIO,这样真的能提升性能吗?”

“you look,当有多有线程调用getInstance()方法的时候,不管三七二十一,先让他们进来。如果fileIO实例不为空,那最好了,直接return实例fileIO,跟synchronized一点都扯不上关系,所以也不会影响到性能。这是双重检验中的第一次检验。”

“oh,I know,如果fileIO是null的,就进入synchronized语句块,在synchronized语句块里面初始化对象。但为什么在synchronized语句中需要再次检查fileIO实例是否为null?”

“这就是第二次检验了,当有多个线程通过第一次检验时,假设线程拿到锁进入synchronized语句块,对fileIO实例进行初始化,释放FileIO.class锁之后,线程二持有这个锁进入synchronized语句块,此时又对fileIO对象就行初始化。所以在这里进行第二次检验防止这种意外发生。”

“我理解了,但我不明白fileIO为什么要用volatile关键字修饰?”

“我们假设线程一进入第二次检验之后就执行FileIO fileIO = new FIleIO()操作,在这个操作中,JVM主要干了三件事

1、在堆空间里分配一部分空间;

2、执行FileIO的构造方法进行初始化;

3、把fileIO对象指向在堆空间里分配好的空间。

但是,当我们编译的时候,编译器在生成汇编代码的时候会对流程顺序进行优化。优化的结果是有可能按照1-2-3顺序执行,也可能按照1-3-2顺序执行。

我们知道,执行完3的时候就fileIO对象就已经不为空了,如果是按照1-3-2的顺序执行,恰巧在执行到3的时候(还没执行2),突然跑来了一个线程,进来getInstance()方法之后判断fileIO不为空就返回了fileIO实例。

此时fileIO实例虽不为空,但它还没执行构造方法进行初始化。又恰巧构造方法里面需要对某些参数进行初始化。后来闯进来的线程糊里糊涂对那些需要初始化的参数进行操作就有可能报错奔溃了。”

6.线程安全-静态内部类

“弄一个单例模式要这么麻烦,写这么多代码,我还是使用饿汉式单例模式算了!”,FileIO抱怨说。

老张笑着说,“那也不能这么想呀,其实我们可以根据使用场景不同来使用不一样的单例模式。如果我们需要在getInstance()方法的时候传入参数进来辅助构造方法初始化,那就得用懒汉式了,比如:“

public static FileIO getInstance(long maxFileSize);

FileIO一想,也对,其他情况就可以使用饿汉式了。

老张好像看出FileIO的想法,道:”其实饿汉式也有其他弊端,比如当我们不想获取FileIO的实例而是想获取其中一个全局变量的时候,在类加载的时候还是会对fileIO实例进行初始化,导致时间比较久。举例如下:

public class FileIO {public static final String TYPE_MP3 = ".mp3";private static final FileIO INSTANCE = new FileIO();private FileIO() {}public static FileIO getInstance() {return INSTANCE;}//... ...
}

当调用FileIO.TYPE_MP3的时候,INSTANCE实例也会被初始化,这显然不是我们需要的。所以,我们Java帝国的高手们又想出了一种叫静态内部类的单例模式,它简单又保证实例能进行懒加载。”

public class FileIO {private static final class FileIOHolder {private static final FileIO INSTANCE = new FileIO();}private FileIO() {}public static FileIO getInstance() {return FileIOHolder.INSTANCE;}//... ...
}

FileIO眼睛一亮,“这个我能理解,当执行getInstance()方法的时候就去调用FileIOHolder内部类里面的INSTANCE实例,此时FileIOHolder内部类会被加载到内存里,在类加载的时候就对INSTANCE实例进行初始化。和饿汉式一个道理,保证了只有一个实例,而且在调用getInstance()方法的时候才进行INSTANCE实例的初始化,又具有懒汉式的部分特性。”

老张满意地说,“是的,但这种写法是利用JVM的机制完成的,在其他语言不一样适用哦!”

7.黑客破坏-反射和反序列化

听了老张一堂课,FileIO收获匪浅,这些都是以前在村里老村长没讲过的一些技巧,它非常高兴,仿佛看到自己在以后踏上人生巅峰、迎娶白富美的样子。

老张决定给它泼泼冷水,“还没完呢,在我们Java帝国,有很多被称为黑客的家伙,他们经常搞破坏,你如果只按照上面介绍的写法使用单利模式,很有可能被破坏哦!”

FileIO愣住了,接着问,“那怎么办呀,他们都是怎么破坏的?”。

老张故作深沉地答,”其实他们的破坏方式无非就是这两种:

1.反射调用构造方法初始化新的变量

private void newInstance() throws IllegalAccessException, InstantiationException {FileIO fileIO = FileIO.class.newInstance();}
​

2.序列化和反序列化产生新的实例

private void serializable() throws IOException, ClassNotFoundException {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName));oos.writeObject(FileIO.getInstance());File file = new File(fileName);ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));FileIO fileIO = (FileIO) ois.readObject();
}

对于通过反射调用构造方法的破坏方式我们可以通过在增加全局变量flag,在第一次初始化的时候就设置为true,第二次初始化的时候判断到flag为true就抛出异常。但这种办法也只能避免破坏,无法彻底阻止,因为他们可以反射flag来修改flag的值。

对于使用序列化和反序列化产生新的实例的方式就容易避免了,可以增加readResolve()方法来预防。我们使用静态内部类的方式来演示如何避免:”

public class FileIO implements Serializable {private static final class FileIOHolder {private static final FileIO INSTANCE = new FileIO();}private FileIO() {}public static FileIO getInstance() {return FileIOHolder.INSTANCE;}private Object readResolve() {return FileIOHolder.INSTANCE;}
}

FileIO不解:”为什么增加readResolve()方法并在里面返回之前创建好的实例就可以避免被反序列破坏呢?“

“这是反序列化机制决定的, 在反序列化的时候会判断如果实现了serializable 或者 externalizable接口的类中又包含readResolve()方法的话,会直接调用readResolve()方法来获取实例。”,老张解释道。

8.终极招数-枚举

FileIO叹一叹气,”虽然单例看起来简单, 但是要弄一个完美的单例模式还是比较麻烦的!“

老张眨了眨眼,“我还有终极招数呢!”

“咦!”

public enum FileIO {INSTANCE;//... ...public File openFile(String fileName) {return getFile(fileName);}
}

“简单吧,这种单例模式是利用枚举来实现的,在调用的时候直接:”

FileIO.INSTANCE.openFile(fileName);

FileIO看着简单又陌生的自己,向老张投去疑惑又质疑的眼神。

老张解释:“别看你现在是枚举类型,但实际上反编译可知枚举实际上就是一个继承Enum的类。所以你的本质还是一个类,因为枚举的特点,你只会有一个实例,同时保证了线程安全、反射安全和反序列化安全。”

FileIO感慨,原来单例也可以这么简单,“你妹的老张,有这么简单的方法你不早告诉我。”

“我们重在过程,不在结果。”

程序员麦兜【编程笔记】-谈谈单例模式相关推荐

  1. 黑马程序员并发编程笔记(二)--java线程基本操作和理解

    3.java进程的基本操作 3.1.创建进程 方法一,直接使用 Thread // 构造方法的参数是给线程指定名字,,推荐给线程起个名字(用setName()也可以) Thread t1 = new ...

  2. 程序员的数学笔记3--迭代法

    第三节课程,介绍的是迭代法. 前两节笔记的文章: 程序员的数学笔记1–进制转换 程序员的数学笔记2–余数 03 迭代法 什么是迭代法 迭代法,简单来说,其实就是不断地用旧的变量值,递推计算新的变量值. ...

  3. 程序员的数学笔记2--余数

    上一节程序员的数学笔记1–进制转换是介绍了进制,特别是十进制和二进制之间的转换,移位操作和逻辑操作. 今天介绍的是余数,看完本节笔记,你会发现生活中有很多东西都有余数的影子. 余数 余数的特性 整数是 ...

  4. C/C++程序员的编程修养

    关注.星标公众号,不错过精彩内容 作者:陈浩 转自:嵌入式云IOT技术圈 什么是好的程序员?是不是懂得很多技术细节?还是懂底层编程?还是编程速度比较快?我觉得都不是.对于一些技术细节来说和底层的技术, ...

  5. 论一个程序员的编程修养(你品,你细品)

    论一个程序员的编程修养 转自:陈浩 芯片之家 作者:陈浩 来源:嵌入式云IOT技术圈 什么是好的程序员?是不是懂得很多技术细节?还是懂底层编程?还是编程速度比较快?我觉得都不是.对于一些技术细节来说和 ...

  6. c++ 读陈黎娟的《C、C++实践进阶之道,写给程序员的编程书》所感--关于变量的类型与引用

    最近在看陈黎娟的书<C.C++实践进阶之道,写给程序员的编程书>,其中第十一章讲的是变量和类型,现做笔记如下: 数据存储区域分只读数据区(存储常量和恒值,正常情况下不做修改).全局/静态存 ...

  7. python好学吗 老程序员-使用 Python 会降低程序员的编程能力吗?

    某些情况下会降低编程能力,某些情况下会提升编程能力,要看你怎么理解"编程能力". 1.使用 Python 会降低程序员的编程能力,这个假设成立的情形 如果强行要说使用Python可 ...

  8. 好程序员大数据笔记之:Hadoop集群搭建

    好程序员大数据笔记之:Hadoop集群搭建在学习大数据的过程中,我们接触了很多关于Hadoop的理论和操作性的知识点,尤其在近期学习的Hadoop集群的搭建问题上,小细节,小难点拼频频出现,所以,今天 ...

  9. 如何向新手程序员介绍编程?

     如何向新手程序员介绍编程? 学习Java,他们都说很easy. 作为一名刚从斯康星大学麦迪逊分校计算机科学系毕业的大学生,我通过一些编程课程认识了很多使用Java的朋友.现在很多学校都在从别的编 ...

  10. 从产品与程序员打架事件,谈谈需求管理的沟通能力

    原标题:从产品与程序员打架事件,谈谈需求管理的沟通能力 昨天一个平安科技内部两名员工打架的视频在互联网圈里疯狂,据传打架原因是产品经理给开发人员提了一个需求:要求app的主体颜色可以随着用户手机壳颜色 ...

最新文章

  1. rrdtool数据备份与迁移
  2. c语言作业 统计成绩,C语言作业 输入多名学生3门课程成绩,并统计成绩的平均分和总分,并根据总分输出名次。...
  3. 高性能 HTML5 地铁样式的应用程序中的内容
  4. 踩坑之路anaconda创建虚拟环境
  5. ModuleNotFoundError: No module named '_ctypes' ERROR:Command errored out with exit status 1: python
  6. html站内消息列表,WebSocket实现站内消息实时推送
  7. BP算法:原理推导数据演示项目实战1(转)
  8. 深度学习TF—14.WGAN原理及实战
  9. 电容或电感的电压_电工入门基础之电容、电感
  10. SpringCloud-粪发涂墙90
  11. Opencv之给图片加水印
  12. python求1到100偶数和_python 求1-100之间的奇数或者偶数之和的实例
  13. slf4j没有在linux中生成日志,slf4j日志记录问题 - 未生成日志文件
  14. 自动化运维工具——puppet详解(二)
  15. linux mint 19.1 安装steam, 打开无反映的解决办法
  16. WKWebView 使用及注意点(keng)
  17. 记录一次net.ipv4.tcp_tw_recycl快速回收机制导致的tcp连接失败问题
  18. 【论文投稿】(一)新手向投稿准备
  19. php竞赛,PHP实现炸金花游戏比赛
  20. HTML学习之四CSS盒子

热门文章

  1. 字符流(FileReader,FileWriter,BufferedReader,BufferedWriter)
  2. 软盘结构及软盘数据的读取
  3. 我,【MySQL】高级篇,一个让你的数据管家单车变摩托的 “关系型数据库”
  4. 介词for和with 和of的用法_常用介词用法(for to with of)
  5. SNTP原理讲解 客户端 C语言实现
  6. 6月份国内外汽车销量趋势分析
  7. Adaptive AUTOSAR (AP) 平台设计(3)——操作系统
  8. 【图解二叉树】如何用中序遍历一棵二叉树?(三种解法)
  9. 信息学奥赛一本通——1011:甲流疫情死亡率
  10. 【JavaScript 逆向】数美滑块逆向分析