文章目录
设计模式之单例模式
单例模式(Singleton Patten)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点的设计模式。单例模式属于创建型模式。单例模式有三个特点:
- 在任何情况下访问该类的实例,都能且只能访问到唯一的一个实例;
- 要访问那个唯一的实例,全局只有一个访问点;
- 单例模式的构造方法是私有化的;
要做到第一点其实不容易,我们知道要实例化一个java对象有多种方式,比如通过new关键字、通过反射、通过反序列化、通过克隆都可以实例化一个java对象,最常见的通过new就可以实例化对象,所以要想实现只有唯一的一个实例,就必须把构造方法私有化,这也是上面说的第三个特点。当然,只把构造方法私有化并不能完全阻止别人去实例化该类,通过反射获取权限后就可以调用私有化的构造方法,下面详细说一下单例模式的几种写法以及如何写出一个安全的、低消耗的单例模式。
饿汉式单例
饿汉式单例是指在单例类首次加载的时候就创建实例,不管你用不用,先创建了再说,像饿汉一样,先吃饱再说。
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
//或者下面这种也行,用静态代码块,也是类加载的时候就初始化实例。
/*private static final HungryStaticSingleton hungrySingleton;
static {
hungrySingleton = new HungryStaticSingleton();
}*/
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
这种饿汉式单例的写法很简单也很常见,他的优缺点如下:
- 优点:
- 没有加任何的锁、执行效率比较高;
- 绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题;
- 缺点:
- 类加载的时候就初始化,不管用还是不用,它都占着空间,浪费了内存,有可能占着茅坑不拉屎;
- 它可以被反射给破坏,创建多个实例。
懒汉式单例
懒汉式单例是指类加载的时候并不初始化实例,只有被外部调用的时候,才去初始化这个唯一的实例。相对于饿汉式单例,他不会浪费内存空间,启动时不创建也节省了时间。
懒汉式单例有三种写法:
简单懒汉式单例
public class LazySimpleSingleton {
private LazySimpleSingleton() {
}
private static LazySimpleSingleton lazy = null;
public static LazySimpleSingleton getInstance() {
if (lazy == null) {
lazy = new LazySimpleSingleton();
}
return lazy;
}
}
这种写法看似没问题,是饿汉式的啊,调用getInstance方法的时候才初始化实例,而且加了判断,不会重复创建,确实,这种写法在单线程的时候是没问题的,但在多线程的情况下,假如有两个线程同时走到了 if (lazy == null) 这一句,两个线程这时候判断都是空,那么他们都会进去if判断里面,然后假如一个线程走完了,return了一个实例,然后第二个线程又初始化了一遍,返回了不同的实例。
那么怎么保证线程安全呢?可以用synchronized关键字来对getInstance方法进行加锁,保证每次只有一个线程调用获取实例方法。
public class LazySimpleSingleton {
private LazySimpleSingleton() {
}
private static LazySimpleSingleton lazy = null;
public synchronized static LazySimpleSingleton getInstance() {
if (lazy == null) {
lazy = new LazySimpleSingleton();
}
return lazy;
}
}
这样就保证了线程安全,多线程同时调用的时候不会返回不同的实例。但是这种方式的缺点也很明显,就是性能问题,因为用了重量级锁synchronized,synchronized关键字加在static修饰的方法上,会导致整个类都被锁住,不可避免的存在一些性能问题,这时候就有了double check写法。
DoubleCheck单例
我们为了避免把整个类锁住,就不把synchronized加在static修饰的获取类实例的getInstance方法上,而是加在new对象的时候。
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazy = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazy == null) {
synchronized (LazyDoubleCheckSingleton.class) {
lazy = new LazyDoubleCheckSingleton();
}
}
return lazy;
}
}
这时候就不会把整个类锁住,提高了执行效率,但是还是存在线程安全问题,比如有两个线程同时走进了if (lazy == null),然后一个线程进入同步代码块初始化对象,执行完毕后,另一个现场也会进入同步代码块再创建一个新对象,所以不是线程安全的,这时候就需要再加一层判断。
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazy = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazy == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazy == null) {
// 1.分配内存给这个对象
// 2.初始化对象
// 3.设置lazy指向刚分配的内存地址
// 4.初次访问对象
// 可能指令重排序导致创建不同的实例。
lazy = new LazyDoubleCheckSingleton();
}
}
}
return lazy;
}
}
这样的话就算有多个线程同时进入了第一个if (lazy == null) ,也会被第二个if判断挡住,不会创建多个实例,是线程安全的。
下面这种写法相对于上面那个,除了多加了一层判断外,也给单例加了个volatile关键字,是因为虽然用了synchronized锁,但在JVM层面去new一个实例时有很多个步骤,指令重排可能导致创建不同的实例,所以用volatile阻止指令重排,保证实例对线程的可见性。
静态内部类单例
虽然双重检查锁单例保证了线程安全,又不会让整个类锁住,但他毕竟用了重量级锁synchronized,那有没有不用synchronized就能既安全又快速得创建单例呢,可以,利用java静态内部类加载的特性来实现。
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
if (LazyHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个实例");
}
}
// static 是为了使单例的空间共享
// final 保证这个方法不会被重写,重载
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}
// 默认不加载
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
private static class LazyHolder静态内部类默认是不加载的,只有在使用getInstance()获取单例的时候,才会先初始化内部类,初始化LazyInnerClassSingleton实例,再返回该实例。这种写法兼顾饿汉式的内存浪费,也兼顾synchronized性能问题,完美地屏蔽了这两个缺点,可以说是最牛B的单例模式的实现方式。
总结一下三种懒汉式单例
- 简单懒汉式单例:关键点是用synchronized锁住获取实例的方法。优点是不用类加载的时候就初始化,节省了空间和时间,也保证了线程安全问题,缺点是用synchronized关键字锁住了static方法,可能会把整个类锁住,存在性能问题;
- DoubleCheck单例:关键点是synchronized加在获取实例方法的内部,不会锁住整个类,先判空,在锁里面再次判空。优点相对于简单懒汉式单例不会把类锁住,减轻性能问题,缺点是还用了synchronized(原罪哈哈哈);
- 静态内部类单例:关键点是构造一个静态内部类,由于内部类特殊的加载方式,只有访问类的时候才会去初始化内部类,避开了饿汉式的缺点,又不存在线程安全问题,没用synchronized所以也不会存在性能问题;
那么静态内部类单例是不是就是完美的呢?答案是否定的,不管是懒汉式单例、饿汉式单例、双重检查锁单例、静态内部类单例,它们都可以通过反射调用构造方法的方式来创建实例,所以上面在构造方法里抛出了异常,来防止想装逼的人用反射生成实例。那有了这个措施之后就是完美的单例,就绝对不会被破坏而生成多个实例了吗?答案还是否定的,就算以上所有写法,统统在私有的构造方法里加入了抛异常机制来防止反射生成实例,也不能完全防止生成多个实例。
反序列化破坏单例
我们做一个测试,先通过getInstance方法获取一个单例,把这个实例用序列化的方式写入磁盘成为一个文件,再通过反序列化的方式把这个对象读进来,那么我们会发现通过getInstance获取的实例和反序列化出来的实例不是同一个。
public void testEqual() throws Exception{
//获取到单例对象
SingletonSerializable instance = SingletonSerializable.getInstance();
//序列化
FileOutputStream fileOutputStream = new FileOutputStream("temp");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(instance);
//反序列化
FileInputStream fileInputStream = new FileInputStream("temp");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
SingletonSerializable o = (SingletonSerializable)objectInputStream.readObject();
//打印结果
System.out.println(instance == o);//false
}
至此你的内心也许会有千万头草泥马在奔腾,想写一个单例就那么难吗?经过深入分析,反序列化的时候调用objectInputStream.readObject()方法获取对象时,一步一步进去看源码,发现里面的确会创建对象,但是后面又加了判断,如果单例类有一个readResolve方法的话,会调用这个方法,取代刚才新创建的对象,那么当我们在单例类里重写readResolve方法,返回单例后,就能避免反序列化生成不同的实例。(详细的请看这篇文章:https://blog.youkuaiyun.com/wenbin516/article/details/85739015 )
注册式单例
上面我们通过重写readResolve方法来避免反序列化,但实际上还是创建了多次实例,只是被readResolve方法返回的覆盖了,返回了同一个,那就没办法不多次创建,只创建一次,返回同一个吗?这里的注册式单例就可以满足。
注册式单例是指将每一个实例都缓存到一个统一的容器中,使用唯一的标示获取实例。
注册式单例分为两种,一种枚举式单例,一种容器式单例。
枚举式单例
把单例类写成一个枚举,getInstance返回一个枚举就行了。
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
//为了方便测试,写了set get方法,实际上不用data变量和set get,只定义一个INSTANCE枚举和一个getInstance方法就可以
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
在JDK层面,JDK源码里限制了枚举类的反序列化和用反射方法调用构造方法,所以是绝对线程安全又快速的。JDK内部会把枚举写到一个容器里,用类名和枚举名映射唯一的一个实例,获取单例的时候,用类名和枚举名找到那个唯一的实例,所以枚举单例是注册式的。枚举单例是目前综合起来最高效最省事最安全的单例模式。
(用反射去创建枚举单例的时候,会抛出cannot reflectively create enum objects异常,通过反序列化的方式获取实例的时候,也返回的是唯一的那个实例,JDK源码专门为枚举单例做了限制,就是为了防止通过反射和反序列化恶意破坏单例。详细的可以看一下反射时调用的newInstance方法源码和 反序列化时的readObject0方法,里面有对枚举进行单独判断。)
容器式单例
容器式单例是指把所有单例都放在一个自己构造的容器里,用相关的标示符去获取单例,它并不是最快速最安全的做法,但这种做法方便对象的管理,比如spring的IOC容器就是这种思路。容器式单例其实也是懒加载,存在线程安全问题,得用synchronized关键字加锁。
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className) {
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
}
这种写法同样可以被反射和反序列化破解,同样可以用在构造方法里抛异常和重写readResovle方法来补救,他有诸多的缺点,但是优点也是其他写法没有的,就是灵活方便,一个容器里可以放很多个单例,用同一个工具类(如ContainerSingleton)去获取。
ThreadLocal单例
ThreadLocal单例本质上也是容器式单例,也是事先初始化一个容器,注册好关键字对应单例,他是伪线程安全的,就是在同一线程里获取到的总是一个实例,在不同线程间获取到的总是不同的实例。
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton() {
}
public static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
}
这个容器是ThreadLocal里面的ThreadLocalMap,这种写法不经常用,可以应用在多数据源实时切换的场景上。
总结
要掌握单例模式,必须掌握以下几点:
- 单例模式必须私有化构造器;
- 必须保证线程安全;
- 延迟加载很重要;
- 防止序列化和反序列化破坏单例;
- 防御反射攻击破坏单例;
总结起来,单例模式有以下几种实现:
- 饿汉式单例
- 简单懒汉式单例
- DoubleCheck单例
- 静态内部类单例
- 枚举式单例
- 自定义容器式单例
- ThreadLocal单例
在使用场景上,不推荐用1,因为空间消耗过大,也不推荐用2,因为同步方法消耗过大,推荐使用3或者4,优先推荐使用4,因为简单、节省内存、线程安全,5的话不常见,如果对单例类没有特殊要求,可以定义为枚举的话可以优先使用,因为它天生自带防止反序列化、防止反射,6和7用在特殊的场景上,如spring的IOC容器,动态配置多数据源。
需要注意的是,除了枚举式单例不需要考虑被反射攻击和反序列化攻击外,其他的写法都需要考虑,把构造器私有化的前提下,在构造器里抛出异常,防止反射生成实例,同时要重写readResovle方法,防止反序列化破坏单例。另外DCL(双重检查锁)单例要用volatile关键字修饰,防止高并发导致锁失效。