单例模式
所谓的单例模式,就是在任何情形下一个类都只能获取到一个实例,并且提供一个全局的访问点。
饿汉式单例模式
饿汉式单例模式在类加载之初就会进行初始化,所以天生是线程安全的。
缺点 :一开始就初始化所有实例, 浪费内存
public class HungrySingleton {
private static final HungrySingleton singleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance(){
return singleton;
}
}
还可以使用静态代码块来实现
懒汉式单例模式
优化了饿汉式单例模浪费内存的缺点,只有在使用到的时候才会进行初始化
synchroized线程安全
为了保证线程安全,对于getInstance方法加了synchronized 关键字
public class SimpleLazySingleton {
private static SimpleLazySingleton singleton = null;
private SimpleLazySingleton() {}
public synchronized static SimpleLazySingleton getInstance() {
if (singleton == null) {
singleton = new SimpleLazySingleton();
}
return singleton;
}
}
缺点: 由于对整个getInstance()方法都加了synchronized 关键字,当大量线程访问该方法的时候,会导致锁竞争,大量线程发生阻塞而影响性能
双重校验 double check
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton singleton = null;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (singleton == null) {
synchronized (DoubleCheckSingleton.class) {
if (singleton == null) {
//因为存在指令重排序的可能,需要加volatile关键字
//1) memory = allocate(); 分配对象的内存空间指令
//2) ctorInstance(memory); 初始化对象
//3) instance = memory; 将已分配存地址赋值给对象引用
singleton = new DoubleCheckSingleton();
}
}
}
return singleton;
}
}
缺点: 虽然发生锁竞争的可能性大大降低,但是还是会存在锁竞争
指令重排之后,instance指向分配好的内存(步骤3)放在了前面,而这段内存的初始化的指令被排在了后面,在线程 T1 初始化完成这段内存之前,线程T2 虽然进不去同步代码块,但是在同步代码块之前的判断就会发现 instance 不为空,此时线程T2 获得 instance 对象(此时这个对象是没有完成初始化的),如果直接使用就可能发生错误。
内部类
解决了懒汉式单例线程安全和饿汉式单例内存浪费的缺点
public class LazyInnerSingleton {
private LazyInnerSingleton() {
}
public static final LazyInnerSingleton getInstance(){
//在调用getInstance的时候才会加载LazyHolder
return LazyHolder.singleton;
}
//在加载LazyInnerSingleton.class的时候 默认不加载
private static class LazyHolder{
private static final LazyInnerSingleton singleton = new LazyInnerSingleton();
}
}
反射和序列化破坏单例
上述几种写法都是可以通过反射破坏单例,为了解决这个问题,在构造方法里加一个限制
public class LazyInnerSingleton {
private LazyInnerSingleton() {
if (LazyHolder.singleton != null) {
throw new RuntimeException("不允许构建多个实例");
}
}
public static final LazyInnerSingleton getInstance(){
//在调用getInstance的时候才会加载LazyHolder
return LazyHolder.singleton;
}
//在加载LazyInnerSingleton.class的时候 默认不加载
private static class LazyHolder{
private static LazyInnerSingleton singleton = new LazyInnerSingleton();
}
}
除此之外,序列化也可以破坏单例,可以通过增加一个readResolve来解决这个问题(虽然解决了返回两个对象的问题,但是实际在代码执行过程中还是初始化了两次,只是最终返回的实例是一个)
public class SerializableSingleton implements Serializable {
private static SerializableSingleton singleton = new SerializableSingleton();
private SerializableSingleton() {}
public SerializableSingleton getInstance() {
return singleton;
}
public Object readResolve() {
return singleton;
}
}
可以看下OIS的源码
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
//...
Object obj;
try {
//判断是否有无参的构造方法,如果有则进行初始化
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
}
//...
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
//判断是否有无参的readResovle方法,有的话则通过反射调用
Object rep = desc.invokeReadResolve(obj);
//...
}
return obj;
}
注册式单例
为了解决上面单例模式被序列化破坏的方式,出现了注册式单例
1. 枚举式
public enum EnumSingleton {
SINGLETON;
public static EnumSingleton getInstance() {
return SINGLETON;
}
}
通过java反编译工具JAD(http://www.javadecompilers.com/jad)反编译EnumSingleton, 查看EnumSingleton.jad可以看到有如下代码:
static
{
SINGLETON = new EnumSingleton("SINGLETON", 0);
$VALUES = (new EnumSingleton[] {
SINGLETON
});
}
枚举式单例是在静态代码块中对实例进行初始化,是饿汉式单例的实现。
不过枚举类天生不能被序列化和反射破坏,可以从源码看出
/**
* Reads in and returns enum constant, or null if enum type is
* unresolvable. Sets passHandle to enum constant's assigned handle.
*/
private Enum<?> readEnum(boolean unshared) throws IOException {
//...
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
//...
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
//通过类对象类和类名找到唯一的枚举实例
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
//...
return result;
}
而Enum是没有无参构造方法的,只有一个protected 的有参构造方法
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
可以看到源码里是禁止对修饰符为Enum的枚举类型进行实例化,会直接抛出Cannot reflectively create enum objects 异常
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
//对于enum类禁止通过反射实例化
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
优点: 不会被反射和序列化破坏,线程安全
缺点: 会在类加载之初就完成实例化,不适合大量使用
2.容器式
类似spring的容器写法
public class ContainerSingleton {
//线程安全
private static Map<String, Object> beanMap = new ConcurrentHashMap<String, Object>();
private ContainerSingleton() {}
public Object getBean(String name) {
synchronized (beanMap) {
if (!beanMap.containsKey(name)) {
try {
Object object = Class.forName(name).newInstance();
beanMap.put(name, object);
} catch (Exception e) {
e.printStackTrace();
}
}
return beanMap.get(name);
}
}
}
容器式单例适合需要大量创建单例的场景,使用了synchronized 保证线程安全,但是多个线程访问同一个对象的时候,会出现锁竞争,影响性能
还可以使用ThreadLocal来实现一个线程内的单例(保证其在单个线程中唯一,天生线程安全). synchronized 通过加锁保证线程安全,以时间换空间,而ThreadLocal将所有对象存储在ThreadLocalMap中,给每个线程提供一个对象,以空间换时间来实现线程隔离
总结:
个人更倾向于内部类实现方式的懒汉式单例模式,虽然可以被反射和序列化破坏单例,但实际工作中也没人故意这么操作去破坏单例
实际使用过程中饿汉式单例也没有什么不好的,它的缺点在于不能延迟加载,但是对于一个肯定要使用的到的资源(没有程序使用我们也不会去创建这个单例),在启动的时候初始化反而比在程序运行过程中去初始化更好(如果资源的创建耗时,反而会影响接口响应)。
单例模式虽然使用起来很方便,不过也存在一些问题:
- 单例会隐藏类之间的依赖关系
- 对代码的扩展性不友好
- 对代码的可测试性不友好
是否要使用单例模式要看你的使用场景,也可以通过工厂模式,spring ioc容器等来保证全局唯一