小小的单例模式也能玩成这样
Posted 东邪日记
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小小的单例模式也能玩成这样相关的知识,希望对你有一定的参考价值。
计算机中,有一个学科叫:设计模式,灵感来源于建筑学。设计模式中有一个比较重要的模式:单例模式。毫不夸张地讲,几乎所有学过计算机的人都知道这个模式,觉得这个模式比较简单。可是我想,正是因为这个模式的使用太普遍了,才让我们误以为这个模式简单到令人发指。从这个简单的模式中,其实能引出很多知识点:
多线程
加锁机制
对象初始化过程
静态内部类
枚举
即时编译器
那怎么理解这个单例模式呢?
用大白话简单定义就是:一个系统中,如果只能允许一个对象存在,则称这种情况是单例模式。比如,
在一夫一妻制的社会中,一个家庭只能允许有一个父亲(母亲)角色的存在,若存在多个,就可能(一定)出问题;
在一个公司中,只能有一个CEO,否则在公司治理上,也可能(一定)出问题,等等。
如何实现这个单例模式呢?
考虑的角度不同或者附加的维度不同,将需要不同的实现方式,但是都必须满足的一个前提是:不允许外部构造(构造器私有化),只能内部构造,并提供对外调用方法。
如果系统是单线程的,则简单的一种实现方式如下:
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {}
}
如果考虑到类加载的效率问题(上述实现方式,是在类被虚拟机加载的时候进行实例化操作),则需要延迟类的实例化,实现方式如下:
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
在单线程环境中,以上实现方式,已经做到了简单明了,写法优雅。但是,如果换个角度,考虑到多线程环境,以上的实现方式就会出大问题:不能实现我们想要的“单例”效果。
举个例子,如果一个父亲有多个孩子(双胞胎也行),夜里,这几个孩子同时哭了,都需要父亲喂奶,假如孩子们自己可以朝着这个“系统”索要父亲,如果用上述的实现方式,则导致系统中被构造了多个父亲。为什么会发生这种情况呢?
因为当多个孩子“线程”同时调用getInstance()方法时,都有可能判断出父亲实例都是null,此时,都会走instance = new Singleton()这段代码,这样的话,就构造出了多个父亲实例,而系统中出现多个父亲,就出了大问题了。那如何改造以上代码呢?
按正常思维思考,我们都会想到:加把锁。当一个线程通过后,立马把门锁起来,不允许其他线程通过,等到屋子里的线程处理完成之后,再把锁打开,放正在门外等候的某一个线程通过,以此类推。
当想到加锁之后,剩下来的问题是:把锁加在哪儿呢?简单的方法是加到getInstance()方法上,实现方式如下:
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
但是,人类永远是不满足的,尤其是对效率的追求。 上述实现方式,有个大问题就是:每次调用getInstance()这个方法时候,都得开锁,这大大牺牲了效率。所以,我们就想呀,能不能把加锁的时机延迟,粒度细化?
当然是可以的:给代码块加锁,实现方式如下:
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
我们又想,还能不能再延迟一点,粒度再细化一点,如下:
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
但是,可惜的是,通过分析(省略),以上实现方式是有问题的,存在两个问题:
第一个问题:
instance = new Singleton()这句代码不是“原子性操作”,这句代码虽然简短,却需要虚拟机干三件事情:
1) 给对象分配内存
2) 调用 Singleton 的构造函数来初始化成员变量,形成实例
3) 将对象指向分配的内存空间
而上述三件事情,执行顺序不可确定,因为存在指令重排的优化,也就是说,当多个线程同时执行if(instance==null)时,对象可能还没有初始化完成,但是判断结果是false,直接返回了,造成紊乱。那有没有什么方法解决呢?在JVM1.5以及以上版本,使用volatile关键字可以禁止指令重排。
第二个问题:
对象未被实例化完成之前,当多个线程同时到达代码if(instance == null)处,判断都将是true,也就是说,都将进入if代码块,实例化对象,这将导致对象被重复实例化。解决方法是:在同步块再进行一次判空处理。
代码实现如下:
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这种实现单例模式的方式被称为双重检查。个人觉得这种实现方式最大的问题有三点:实现起来复杂、代码不优雅、受虚拟机版本限制。我想,也正是因为这样,大神Joshua Bloch在《Effective Java》这本书第二版中描述的单例模式是如下两种:
第一种:静态内部类实现单例
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
第二种:枚举实现法
public enum Singleton {
INSTANCE;
}
上述两种实现方法,从线程安全、代码优雅角度来说,都是近乎完美的(涉及到两个知识点的理解:静态内部类、枚举)。
最后,希望我理解单例模式的方式,能系统化那些杂七杂八的单例模式介绍!
后续思考:
1. 如果在不同的自定义类加载器(Class Loader)中,该如何保证单例呢?
2. 如果在不同的虚拟机中,该如何保证单例呢?
3. 如果对象是通过反序列化生成,又改如何保证单例呢?
--EOF--
推荐阅读:
以上是关于小小的单例模式也能玩成这样的主要内容,如果未能解决你的问题,请参考以下文章