概述
单例模式算是我接触设计模式这种思想所学习的第一个设计模式。记得刚入行时面试,面试官总是会让写一种单例模式的实现。这篇文章主要是来总结一下单例模式的几种实现以及每一种实现的优缺点,旨在领会每一种写法,真正明白他们的区别,免得以后尴尬。Mark。
定义
按照设计模式中的定义,Singleton模式的用途是"ensure a class has only one instance, and provide a global
point of access to it"
(确保每个类只有一个实例,并提供它的全局访问点)
故名思义,就是这个类在当次整个系统中只存在一个实例,所有的访问必须通过这个唯一的实例来进行调用
一、懒汉式
之所以称为懒汉式,是基于这个类的实例是否能够按需加载,也就是懒加载。
代码实现
public class LazySingleton {
private static LazySingleton lazySingleton;
privite LazySingleton(){}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
这种是最基本的实现,但是这种方式在多线程并发中是有问题的,不是唯一的实例。
假如A/B两个线程第一次同时访问这个getInstance方法获得实例,然而这个条件(instance == null)有可能同时都成立(并发执行),那么线程A和线程B会分别获得一个类的对象,这样的话,这个类的单例就失去意义了。
策略评价:
优点是可以实现延迟加载。在类初次加载的时候,由于只是声明了这个静态的对象,但不会自动初始化 instance对象,所以称为懒汉式
缺点是线程不安全。多线程并发无法保证唯一的实例
改进策略:需要保证线程安全
二、饿汉式
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton ();
privite HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton ;
}
}
策略评价:
线程安全。利用了类的加载机制,加载初始化静态变量,且被只会执行一次,且JVM会利用锁来同步多个线程对同一个类的初始化。这样就保证了构造方法只会调用一次。
不能懒加载。无论是否使用都会去初始化实例。
改进策略:需要懒加载,请看懒汉线程安全式
注意到一点,懒汉模式在申明lazySingleton 的时候没有加final关键字,但是饿汉模式加了。那饿汉模式为什么要加final,什么时候加?
final关键词是代表此变量一经赋值,其指向的内存引用地址将不会再改变。加final也仅仅是表示类加载的时候就初始化对象了。比不加载final要早一点。
如果存在释放资源的情况下,就不能加final修饰了,释放资源之后,如果需要重新使用这个单例,就必须存在重新初始化的过程,而final定义的常量是不能重新赋值的,所以不能加final,对于不需要释放资源的情况,可以加final
总而言之,要不要加final修饰,可以根据情况而定。
懒汉模式为什么不加final,是因为被final修饰的变量需要直接赋值,或者在静态代码块中赋值,这样就不是懒加载模式了
三、懒汉线程安全式
public class LazySafetySingleton {
private static LazySafetySingleton lazySafeSingleton;
privite LazySafetySingleton (){}
public static synchronized LazySafetySingleton getInstance(){
if(lazySafeSingleton== null){
lazySafeSingleton= new LazySafetySingleton ();
}
return lazySafeSingleton;
}
}
public class LazySafetySingleton{
private static LazySafetySingleton lazySafeSingleton;
privite LazySafetySingleton (){}
public static LazySafetySingleton getInstance() {
synchronized (LazySafetySingleton.class) {
if (lazySafeSingleton == null) {
lazySafeSingleton = new LazySafetySingleton();
}
}
return lazySafeSingleton ;
}
}
策略评价:
线程安全。使用了synchronized同步锁。
懒加载。不会在类加载就初始化。
显然这种同步性能很低。由于使用了同步锁,所以每次调用getInstance都会进行同步。其实我们只想第一次(instance == null)进行同步,初始化成功后,以后每次都直接返回就行了。
改进策略:DCL
四、double-check-locking
public class DclSingleton {
private volatile static DclSingleton dlcSingleton;
privite DclSingleton (){}
public static DclSingleton getInstance() {
if(dlcSingleton== null){
synchronized (DclSingleton .class){
if(dlcSingleton == null){
dlcSingleton = new DclSingleton();
}
}
}
return dlcSingleton ;
}
}
这种策略看似解决了每次都需要同步的问题,但是由于 标记4处 instance = new DclSingleton(); 这个初始化是非原子性的操作。就是说这个在JVM中可能会分成几步执行,那么就会存在指令重排序的问题,所以需要继续改进 声明处添加 volatile 关键字。
改进后,懒加载
线程安全
五、静态内部类
public class StaticInnerSingleton {
privite StaticInnerSingleton(){}
public static StaticInnerSingleton getinstance(){
return SingletonHolder.staticInnerSingleton;
}
public static class SingletonHolder{
private static final StaticInnerSingleton staticInnerSingleton = new StaticInnerSingleton();
}
}
策略评价:
这种静态内部类的实现,主要是运用类的加载机制来保证线程的安全(原因见懒汉式的线程安全优点)。同时这个内部类与外部类没有绑定关系,而且只有外部类调用的时候才会加载,所以能做到懒加载。
但是如果实例化的时候需要传参就很不方便了,比如需要传context。所以不需要传参的时候建议使用该方法
六、枚举
public enum EnumSingleton {
INSTANCE;
public void doSomeThing() {
//在此处进行实例化对象
}
}
策略评价:枚举实现是目前比较建议的方法,和他获取单例的方法比起来,有两个明显的优势:
- 避免反射攻击,其他方式实现的单例都可以通过反射拿到构造器,通过构造器获取实例。但获取的实例和通过正常方式获取的实例不是同一个对象。枚举单例模式无法通过反射获取实例,因为Constructor类的newInstance方法源码中有判断,要实例化的类是否是枚举,如果不是,才能实例化(14行)
1 public T newInstance(Object ... initargs)
2 throws InstantiationException, IllegalAccessException,
3 IllegalArgumentException, InvocationTargetException
4 {
5 if (!override) {
6 if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
7 Class<?> caller = Reflection.getCallerClass();
8 checkAccess(caller, clazz, null, modifiers);
9 }
10 }
11 if ((clazz.getModifiers() & Modifier.ENUM) != 0)
12 throw new IllegalArgumentException("Cannot reflectively create enum objects");
13 ConstructorAccessor ca = constructorAccessor; // read volatile
14 if (ca == null) {
15 ca = acquireConstructorAccessor();
16 }
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
- 避免序列化
传统单例存在的另外一个问题是一旦你实现了序列化接口,那么它们不再保持单例了,因为readObject()方法一直返回一个新的对象就像java的构造方法一样。但枚举实现的单例,即使序列化,获取的对象依然保持单例。
网友评论