模式简介
单例模式(Singleton),创建型模式,采用单例模式创建的类,只可能产生一个实例供外部访问,并提供一个全局访问点。
模式特点
- 单例类只有一个实例;
- 单例类必须自己创建自己的唯一实例;
- 单例类必须给所有对象提供自己创建的实例
应用场景
当某个类被抽象出来后具有类似资源管理器这一功能时,我们就可以考虑采用单例模式来实现。
代码示例
懒汉式
只有到使用的时候才进行创建该单例类的实例(该类对象不存在时),由于这里存在一个创建的操作,所以当并发量大的时候,懒汉式这种实现就是线程不安全的了。
1 | package com.rainmonth.pattern.creational.singleton; |
以上代码模拟一个账户管理,对外暴露存款和提现两个方法,采用单例模式实现,但上面的代码对于一个高并发的系统是不合格的,因为它不是线程安全的。
线程安全概念:
如果你的代码所在的进程中有多个线程同时运行,而这些线程可能同时运行某段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他变量的值也和预期的是一样的,就是线程安全的。
或者说:
一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程间切换不会导致该接口的执行结果存在二义性,也就说我们不用考虑同步问题了,那么这个类或程序就是线程安全的。
Todo,模拟出线程不安全的情况…
对于懒汉式所带来的线程不安全问题,有多种解决方式,具体见代码:
直接在getInstance方法上加synchronized关键字:
1
2
3
4
5
6public static synchronized AccountManager getInstance() {
if (null == accountManager) {
accountManager = new AccountManager();
}
return accountManager;
}这样写虽然解决了线程不安全问题,但getInstance这个方法调用很频繁时,就会出现同步方法频繁调用的问题,影响效率。
避免直接在获取单例的getInstance方法上加同步:
1
2
3
4
5
6
7
8
9
10public static AccountManager getInstance() {
if (null == accountManager) {
synchronized(AccountManager.class) {
if (null == accountManager) {
accountManager = new AccountManager();
}
}
}
return accountManager;
}或另外一种写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public static AccountManager getInstance() {
if (null == accountManager) {
syncInit();
synchronized(AccountManager.class) {
if (null == accountManager) {
accountManager = new AccountManager();
}
}
}
return accountManager;
}
private static synchronized void syncInit() {
if (null == accountManager) {
accountManager = new AccountManager();
}
}二者是等效的,避免了频繁调用同步方法的问题。
饿汉式
单例类一创建,就初始化该类的对象。问题当然就是有可能存在内存资源浪费了,不过这样是线程安全的,具体实现:
1 | public class AccountManager { |
静态内部类形式
采用静态内部类的方式来实现单例
1 | public static AccountManager getInstance() { |
上面这种写法,原理和饿汉式差不多,不过它实现了懒加载,即当AccountManager被加载时,AccountManager的实例并没有被创建,而是当调用AccountManager的getInstance方法是,才进行AccountManager实例的创建。
枚举
利用单元素的枚举来实现单例模式,代码如下:
1 | public enum AccountManagerEnum { |
使用如下:
1 | AccountManagerEnum.INSTANCE.getInstance(); |
用单元素枚举实现单例的好处:
- 线程安全;
- 可以防止反序列化重新创建新的对象;
那么是如何保证的呢?
首先,枚举中构造方法为私有,在访问实例时,会执行私有构造方法,由于每个枚举实例都是static final 类型的,所以只能被实例化一次。也就是说因为enum中的实例保证只被实例化一次,上面的INSTANCE也被保证为只实例化一次。
然后,可以看看Enum类的声明:
1 | public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { |
显然,枚举也提供了序列化机制。
注意问题
以上面说的单例类只存在一个实例对象是忽略了Java反射这种情况的,因为通过Java反射机制,即使构造方法为私有的,也可以通过其构造出该类的对象。