定义
确保某一个类
只有一个实例
,
而且自行实例化
并向整个系统
提供这个实例
。
使用场景
确保某个类有且只有一个对象的场景,
避免产生多个对象消耗过多的资源,
或者
某种类型的对象只应该有且只有一个。
例如,
创建一个对象需要消耗的资源过多,
如要访问IO和数据库等资源,这时就要考虑使用单例模式。
单例模式UML类图
- 角色:
(1)Client——高层客户端;
(2)Singleton——单例类。
-
实现单例模式的关键点:
(1)构造函数
不对外开放,一般为Private
;
(2)通过一个静态方法
或者枚举
返回单例类对象
;
(3)确保单例类的对象
有且只有一个,尤其是在多线程环境
下;
(4)确保单例类对象
在反序列化时
不会重新构建对象
。 -
通过将单例类的
构造函数私有化
,
使得客户端代码不能通过new
的形式手动构造单例类的对象
。 -
单例类
会暴露一个公有静态方法
,
客户端
需要调用这个静态方法
获取到单例类
的唯一对象
; -
在获取这个单例对象的过程中需要确保
线程安全
,
即在多线程环境
下构造单例类的对象
也是有且只有一个
,
这也是实现的难点
。
重点,注意单例模式中 volatile的重要性
单例的几种实现方式
1. 饿汉模式
声明一个静态类对象
,在声明时
就己经初始化
,
用户调用类对象get方法
时,可以直接拿去用;
【一声明就初始化,所谓“饿”】
如下,
CEO类使用了饿汉单例模式;
/**
* 普通员工
*/
class Staff {
public void work() {
// 干活
}
}
// 副总裁
class VP extends Staff {
@Override
public void work() {
// 管理下面的经理
}
}
// CEO, 饿汉单例模式
class CEO extends Staff {
private static final CEO mCeo = new CEO();
// 构造函数私有
private CEO() {
}
// 公有的静态函数,对外暴露获取单例对象的接口
public static CEO getCeo() {
return mCeo;
}
@Override
public void work() {
// 管理VP
}
}
// 公司类
class Company {
private List<Staff> allPersons = new ArrayList<Staff>();
public void addStaff(Staff per) {
allPersons.add(per);
}
public void showAllStaffs() {
for (Staff per : allPersons) {
System.out.println("Obj : " + per.toString());
}
}
}
public class Test {
public static void main(String[] args) {
Company cp = new Company();
// CEO对象只能通过getCeo函数获取
Staff ceo1 = CEO.getCeo();
Staff ceo2 = CEO.getCeo();
cp.addStaff(ceo1);
cp.addStaff(ceo2);
// 通过new创建VP对象
Staff vp1 = new VP();
Staff vp2 = new VP();
// 通过new创建Staff对象
Staff staff1 = new Staff();
Staff staff2 = new Staff();
Staff staff3 = new Staff();
cp.addStaff(vp1);
cp.addStaff(vp2);
cp.addStaff(staff1);
cp.addStaff(staff2);
cp.addStaff(staff3);
cp.showAllStaffs();
}
}
2. 懒汉模式
- 懒汉模式是声明一个静态对象,
并且在用户第一次
调用getInstance
时才
进行初始化
;
【“拖延”,等到调用才初始化,所谓“懒”!】
public class Singleton {
private volatile static Singleton instance;
private Singleton () {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton ();
}
return instance;
}
}
-
getInstance()
中添加了synchronized
关键字,
也就是getInstance()
是一个同步方法,
即上面所说的在多线程
情况下保证单例对象唯一性
的手段。 -
只不过这里可能有一个问题,
即使instance
己经被初始化(第一次调用时就会被初始化instance),
每次
调用getInstance
方法都会进行同步
,
这样会消耗不必要的资源,这也是懒汉单例模式存在的最大问题。 -
优点:单例只有在使用时才会被实例化,在一定程度上
节约了资源
; -
缺点:第一次加载时需要及时进行实例化,反应稍慢,
最大的问题是每次调用 getInstance都进行同步
,造成不必要的同步开销
。
这种模式一般不建议使用!!!!!!!!!!
3. DoubleCheckLock(DCL)实现单例【双重校验锁】
- 优点:
资源利用率高,
第一次执行getInstance()
时单例对象才会被实例化,效率高。
既能够在需要时才初始化
单例,
又能够保证线程安全
,
且单例对象初始化后每次调用getInstance()
不进行同步锁
,
减少不必要的同步开销
: - 缺点:第一次加载时反应稍慢,
也由于 Java 内存模型的原因偶尔会失败。
在高并发环境下也有一定的缺陷,虽然发生概率很小。
public class Singleton {
private volatile static Singleton sInstance = null;
private Singleton() {
}
public void doSomething() {
System.out.println("do sth.");
}
public static Singleton getInstance() {
if (mInstance == null) {
synchronized (Singleton.class) {
if (mInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}
-
static 保证单例;
volatile 禁止重排序;
getInstance() 用来获取实例;
synchronized 保证原子性、可见性、线程安全; -
亮点:
getInstance()
方法中对instance
进行了两次判空:
第一层判断主要是为了避免不必要的同步【有实例则直接返回,没必要同步】,
第二层的判断则是为了在null的情况
下创建实例
【可能第一层与第二层判断中途有其他线程初始化完成了单例,
单例不为null
,就不用创建了】:
假设线程A和线程B先后访问了getInstance()
;
线程A执行到sInstance = new Singleton()
语句,
这里看起来是一句代码,但实际上它并不是一个原子操作
,
这句代码最终会被编译成多条汇编指令,它大致做了3件
事情:
(1)给Singleton的实例分配内存
;
(2)调用Singleton()
的构造函数
,初始化
成员字段;
(3)将sInstance对象
指向分配的内存空间(此时sInstance就不是null
了)。
但是,由于Java编译器允许处理器乱序执行
,
以及JDK1.5之前JMM(Java Memory Model,即Java内存模型)
中Cache、
寄存器到主内存回写顺序的规定,
上面的第二和第三的顺序是无法保证
的。【指令重排序】
即,执行顺序可能是1-2-3也可能是1-3-2。
如果是后者,并且在3执行完毕
、2未执行之前
,被切换到线程B
上,
这时候sInstance
因为己经在线程A内执行过了第三点,
sInstance
己经是非空
了,
所以,
线程B
通过getInstance()
直接取走sInstance
,
再使用时就会出错
,这就是DCL失效问题
,
而且这种难以跟踪难以重现的错误很可能会隐藏很久。
在JDK1.5之后,SUN官方己经注意到这种问题,
调整了JVM,具体化了volatile
关键字,
因此,
如果JDK是1.5或之后的版本,
只需要将sInstance
的定义改成private volatile static Singleton sInstance = null
就可以保证sInstance对象每次都是从主内存中读取
,
就可以使用DCL的写法来完成单例模式
。
当然,volatile
或多或少也会影响到性能
,
但考虑到程序的正确性
,牺牲这点性能还是值得的。^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -
DCL 模式是
使用最多
的单例实现方式!!!!
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
它能够在需要时才
实例化单例对象,
并且能够在绝大多数场景下保证单例对象的唯一性
,
除非代码在并发场景比较复杂或者低于JDK 6版本下使用,
否则,这种方式一般能够满足需求。
4. 静态内部类单例模式
- DCL虽然在一定程度上解决了
资源消耗、多余的同步、线程安全
等问题,
但是,它还是在某些情况下出现失效的问题。
就是刚说的双重检查锁定(DCL)失效
; - 在《Java 并发编程实践》一书的最后谈到了这个问题,
并指出这种“优化”是丑陋
的,不赞成使用
。
而建议使用如下的代码替代
:
public class Singleton {
private Singleton() { }
public static Singleton getInstance () {
return SingletonHolder.sInstance;
}
/**
* 静态内部类
*/
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
- 当第一次加载
Singleton类
时并不会初始化sInstance
,
只有在第一次调用Singleton
的getInstance()
时sInstance
才会被初始化!!!
因此,
第一次调用getInstance()
会导致虚拟机加载SingletonHolder类
,
这种方式不仅能够确保线程安全
,
也能够保证单例对象的唯一性
,同时也延迟
了单例的实例化
,
所以这是推荐使用的单例模式实现方式。
5. 枚举单例
除了以上几种方式,还有更简单的实现方式——枚举!:
public enum SingletonEnum {
INSTANCE;
public void doSomething() {
System.out.println("do sth.");
}
}
- 优点突出:写法简单;
枚举
在Java中与普通的类是一样的,
不仅能够有字段,还能够有自己的方法。
最重要的是默认枚举实例
的创建
是线程安全
的,
并且在任何情况下
它都是一个单例
。
在上述的几种单例模式实现中,
在一个情况下它们会出现重新创建对象
的情况,那就是反序列化
。
通过序列化
可以将一个单例
的实例对象
写到磁盘
,
然后再读回来
,从而有效地获得一个实例
。
即使构造函数
是私有
的,
反序列化
时依然可以通过特殊的途径去创建类的一个新的实例
,
相当于调用该类的构造函数
。
反序列化
操作提供了一个很特别的钩子函数
,
类中具有一个私有的、被实例化
的方法readResolve()
,
这个方法可以让开发人员控制对象的反序列化
。
例如,
上述几个示例中如果要杜绝单例对象在被反序列化时重新生成对象,
那么必须加入如下方法:
private Object readResolve() throws ObjectStreamException {
return sInstance;
}
即在readResolve()
中将sInstance对象
返回,
而不是默认的重新生成一个新的对象。
而对于枚举,并不存在这个问题,
因为即使反序列化
它也不会重新生成
新的实例。
参考:
- 《Android源码设计模式解析与实战》
网友评论