单例模式
来源:互联网 发布:数据挖掘软件容易使用 编辑:程序博客网 时间:2024/06/11 05:47
说到单例模式,大家或多或少都听说过,尤其是懒汉式和饿汉式,几乎每一个Java程序员都信口拈来,但是却不是每个人都能信手拈来,尤其是信手拈来一个性能良好又线程安全的单例模式了。这篇文字旨在总结常见的单例设计模式,并总结一些常跳的坑。
- 懒汉式
- 版本一初识懒汉式
- 版本二方法上锁住你
- 版本三双重校验锁初级版
- 版本四双重校验锁终级版
- 饿汉式
- 静态内部类
- 枚举
懒汉式
版本一,初识懒汉式
很多人提到懒汉式都会写下如下版本:
public class Singleton01 { //我比较懒,先不创建实例 private static Singleton01 instance = null; private Singleton01(){} // 通过private,禁止其他代码创建该对象 public static Singleton01 getInstance() { if(instance == null) { instance = new Singleton01();//到调用创建实例方法的时候再创建并赋值 } return instance; }}
甚至有些教科书或者教学视频也是这么教我们的,(笔者曾经看的某培训班视频就是这么写的,很长一段时间都对此深信不疑),但是如果你在面试或者工程中这么写,那无疑在作死。
原因是,这段代码存在一个致命的问题:在初始阶段(instance未被实例化),当有多个线程并行调用 getInstance()方法 的时候,就很可能创建多个实例。也就是说该版本的单例模式在多线程并发的情况下不能正常工作。对,多线程下它压根就不单例。
现在我们想办法改进该版本。
版本二,方法上锁住你
对多线程稍微有点了解的Java工程师在改进上述代码的时候,都会想到如下方法来解决多线程问题:
public class Singleton02 { private static Singleton02 instance = null; private Singleton02(){} //锁住这个方法,简单粗暴 public static synchronized Singleton02 getInstance() { if(instance == null) { instance = new Singleton02(); } return instance; }}
哈哈!这个简单粗暴的加锁方式肯定是线程安全的了。
但是,它并不高效。不是说锁的粒度太大,而是因为只有在第一次调用getInsance()方法时才会需要同步操作。其余时候再进行同步操作,完全是在浪费性能嘛。
根据这个思路,聪明的程序员们想到了第三种方式。
版本三,双重校验锁初级版
为了让代码更加高效又安全,我们可以使用双重校验锁的模式来实现单例模式:
public class Singleton03 { private static Singleton03 instance = null; private Singleton03() {} public static Singleton03 getInstance() { if (instance == null) {//一次校验 /* 当对象已经被创建的时候,显然下面的代码不会被执行到 */ synchronized(Singleton03.class) {//当对象还没被创建,加块锁控制可能的并发 if (instance == null) {//两次校验 instance = new Singleton03(); } } } return instance; }}
这段代码看起来太巧妙了,甚至可以说完美,即解决了并发安全问题,性能上又没有太大问题。程序员终于可以安心回去睡觉了。
等等!这时经验丰富的长者程序员过来说:你们啊,还是too young。这段代码还有一个地方存在不足!
版本四,双重校验锁终级版
长者接着说:问题主要出在了这么一句:
instance = new Singleton03();
要知道,这一句可不是原子操作!
JVM在执行这一行语句时其实做了三件事:
- 1,给 instance 分配内存空间;
- 2,调用 Singleton03 的构造函数来初始化成员变量;
- 3,将instance对象指向分配的内存空间(执行完这步 instance 才为非 null 了)
“这一句是不是原子操作又有什么关系呢?反正是放在同步代码块里面,其他线程又无法插入”程序员疑惑的问到。
“此言差矣,因为JVM即时编译器存在指令重排序的优化,上面的步骤2和步骤3的先后顺序是无法保证的。”长者说道。
程序员思索片刻后一拍脑袋:“我明白了,假如是按1->2->3的顺序当然是没什么问题的,但是万一被JVM即时编译器优化成1->3->2的顺序,那就可能出现步骤3执行完毕,该线程的时间片正好结束,被另一个线程抢占执行,此时instance**已经非null**,但是却没有进行过初始化操作,这时编译器就会报错!”
“说的非常对,”长者满意道,“这时我们可以考虑用volatile变量来声明instance”。
“对哦”,程序员恍然大悟,“volatile除了我们常用的使内存对其他线程立即可见以外,还有禁止指令重排的功能呢。这下大功告成了!”
public class Singleton04 { //用volatile修饰,禁止汇编层指令重排 private volatile static Singleton04 instance = null; private Singleton04() {} public static Singleton04 getInstance() { if (instance == null) {//一次校验 /* 当对象已经被创建的时候,显然下面的代码不会被执行到 */ synchronized(Singleton04.class) {//当对象还没被创建,加块锁控制可能的并发 if (instance == null) {//两次校验 instance = new Singleton03(); } } } return instance; }}
高兴之余,不忘划一下重点:
volatile的主要作用有两种
- 内存的可见性:即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的;
- 禁止指令重排序:汇编层,volatile 变量的赋值操作后面会有一个内存屏障,读操作不会被重排序到内存屏障之前。
饿汉式
这种情况下,单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
public class Singleton05 { //类加载时就初始化 private static final Singleton05 instance = new Singleton05(); private Singleton() {} public static Singleton05 getInstance(){ return instance; }}
“纳尼?这么简单就可以实现线程安全了?看起来性能也不错啊,不早说?”场外传来了不和谐的声音。
这种写法如果完美的话,就没必要在前面啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式,单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法,如果压根就没有用它,就会造成内存的浪费。最重要的是饿汉式的创建方式在一些场景中也将无法使用:譬如果Singleton05实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它。这是,这种方法就不能满足要求。
静态内部类
这种方法是《剑指offer》和《effective Java》都一致推荐的,让我们看看它究竟有多厉害:
public class Singleton06 { private static class SingletonHolder { private static final Singleton06 INSTANCE = new Singleton06(); } private Singleton06() {} public static final Singleton06 getInstance() { return SingletonHolder.INSTANCE; } }
这种用静态内部类实现单例模式的方法:
- 1,仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此本质上它是懒汉式的;
- 2,同时读取实例的时候不会进行同步,没有性能缺陷;
- 3,不会依赖 JDK 版本。(volatile必须依赖1.5以后的JDK)
枚举
枚举法非常简单:
public enum Singleton07 { INSTANCE; public void whateverMethod() {}}
单元素的枚举类型已经成为实现Singleton的最佳方法。 --《effective java》
《effective java》对枚举法有如此好评。是因为枚举有如下优点:
- 1,可以通过诸如
Singleton07.INSTANCE
来访问实例。没发现吗,这比调用getInstance()方法可简单多了。 - 2,创建枚举默认就是线程安全的。
- 3,防止反序列化导致重新创建新的对象。
- 单例、单例模式
- 单例模式-多线程单例模式
- 单件模式(单例模式)
- 设计模式------单例模式
- 设计模式------单例模式
- 设计模式-单例模式
- 设计模式 - 单例模式
- 设计模式---单例模式
- 设计模式---单例模式
- PHP模式-单例模式
- 【设计模式】单例模式
- 设计模式-单例模式
- 设计模式----单例模式
- 设计模式--单例模式
- 设计模式-单例模式
- 单例模式(单子模式)
- 设计模式-单例模式
- [设计模式] 单例模式
- [PAT甲级]1012. The Best Rank (25)(最好排名)
- 它可以通过前述的 Kafka, Flume等数据源
- android ExpandableListView三级菜单的使用
- 字符串问题---最小包含子串的长度
- Beginning Spring学习笔记——第3章(三)文件上传、异常处理和个性化
- 单例模式
- CORS——跨域请求那些事儿
- AVL 平衡二叉搜索树原理及编程实现 (C++)版本 第二版
- Android Saving Data
- 关于内存泄漏---auto_ptr
- loadrunner11 回放脚本Action.c(94): 错误 -27979: 找不到请求的表单 [MsgId: MERR-27979]
- LR回放https协议脚本失败:[GENERAL_MSG_CAT_SSL_ERROR]connect to host "XXX" failed:[10054] Connection reset by
- android MAGNETIC_FIELD
- Hadoop 2.0 data write operation acknowledgement