看公司代码的时候发现项目中单例模式应用挺多的,并且发现的两处单例模式用的还是不同的方式实现的,那么单例模式到底有几种写法呢?单例模式看似很简单,但是实际写起来问题多多
本文大纲
- 什么是单例模式
- 饿汉式创建单例对象
- 懒汉式创建单例对象
- 单例模式的优缺点
- 单例模式的应用场景
什么是单例模式
确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例
饿汉式创建单例模式
饿汉式创建就是在类加载时就已创建好对象,而不是在需要时在创建对象
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
/**
* 私有构造函数,不能被外部所访问
*/
private HungrySingleton() {}
/**
* 返回单例对象
* */
public static HungrySingleton getHungrySingleton() {
return hungrySingleton;
}
}
说明:
- 构造函数私有化,保证外部不能调用构造函数创建对象,创建对象的行为只能由这个类决定
- 只能通过
getHungrySingleton
方法获取对象 -
HungrySingleton
对象已经创建完成【在类加载时创建】
缺点:
- 如果
getHungrySingleton
一直没有被使用到,有点浪费资源
优点: - 线程安全
懒汉式创建单例模式
懒汉式创建就是在第一次需要该对象时在创建
-
存在错误的懒汉式创建单例对象
根据定义很容易在上面饿汉式的基础上进行修改public class LazySingleton { private static LazySingleton lazySingleton = null; /** * 构造函数私有化 * */ private LazySingleton() { } private static LazySingleton getLazySingleton() { if (lazySingleton == null) { return new LazySingleton(); } return lazySingleton; } }
说明:
- 构造函数私有化
- 当需要时【
getLazySingleton
方法调用时】才创建
嗯,好像没什么问题,但是当有多个线程同时调用getLazySingleton
方法时,此时刚好对象没有初始化,两个线程同时通过lazySingleton == null
的校验,将会创建两个LazySingleton
对象。必须搞点手段使getLazySingleton
方法是线程安全的
-
synchronize
或Lock
很容易想到使用synchronize
或Lock
对方法进行加锁
使用synchronize
public class LazySynchronizeSingleton { private static LazySynchronizeSingleton lazySynchronizeSingleton= null; /** * 构造函数私有化 * */ private LazySynchronizeSingleton() { } public synchronized static LazySynchronizeSingleton getLazySynchronizeSingleton() { if (lazySynchronizeSingleton == null) { lazySynchronizeSingleton = new LazySynchronizeSingleton(); } return lazySynchronizeSingleton; } }
使用
Lock
public class LazyLockSingleton { private static LazyLockSingleton lazyLockSingleton = null; /** * 锁 **/ private static Lock lock = new ReentrantLock(); /** * 构造函数私有化 * */ private LazyLockSingleton() { } public static LazyLockSingleton getLazyLockSingleton() { try { lock.lock(); if (lazyLockSingleton == null) { lazyLockSingleton = new LazyLockSingleton(); } } finally { lock.unlock(); } return lazyLockSingleton; } }
这两种方式虽然保证了线程安全,但是性能较差,因为线程不安全主要是由这段代码引起的:
if (lazyLockSingleton == null) { lazyLockSingleton = new LazyLockSingleton(); }
给方法加锁无论对象是否已经初始化都会造成线程阻塞。如果对象为
null
的情况下才进行加锁,对象不为null
的时候则不进行加锁,那么性能将会得到提升,双重锁检查可以实现这个需求 -
双重锁检查
在加锁之前先判断lazyDoubleCheckSingleton == null
是否成立,如果不成立直接返回创建好的对象,成立在加锁public class LazyDoubleCheckSingleton { /** * 使用volatile进行修饰,禁止指令重排 * */ private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; /** * 构造函数私有化 * */ private LazyDoubleCheckSingleton() { } public static LazyDoubleCheckSingleton getLazyDoubleCheckSingleton() { if (lazyDoubleCheckSingleton == null) { synchronized (LazyDoubleCheckSingleton.class) { if (lazyDoubleCheckSingleton == null) { lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); } } } return lazyDoubleCheckSingleton; } }
说明:
- 为什么需要对
lazyDoubleCheckSingleton
添加volatile
修饰符
因为lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
不是原子性的,分为三步:- 为
lazyDoubleCheckSingleton
分配内存 - 调用构造函数进行初始化
- 将
lazyDoubleCheckSingleton
对象指向分配的内存【执行完这步lazyDoubleCheckSingleton
将不为null
】
- 为
为了提高程序的运行效率,编译器会进行一个指令重排,步骤2和步骤三进行了重排,线程1先执行了步骤一和步骤三,执行完后,
lazyDoubleCheckSingleton
不为null
,此时线程2执行到if (lazyDoubleCheckSingleton == null)
,线程2将可能直接返回未正确进行初始化的lazyDoubleCheckSingleton
对象。出错的原因主要是lazyDoubleCheckSingleton
未正确初始化完成【写】,但是其他线程已经读取lazyDoubleCheckSingleton
的值【读】,使用volatile
可以禁止指令重排序,通过内存屏障保证写操作之前不会调用读操作【执行if (lazyDoubleCheckSingleton == null)
】缺点:
- 为了保证线程安全,代码不够优雅过于臃肿
- 为什么需要对
-
静态内部类
public class LazyStaticSingleton { /** * 静态内部类 * */ private static class LazyStaticSingletonHolder { private static LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton(); } /** * 构造函数私有化 * */ private LazyStaticSingleton() { } public static LazyStaticSingleton getLazyStaticSingleton() { return LazyStaticSingletonHolder.lazyStaticSingleton; } }
静态内部类在调用时才会进行初始化,因此是懒汉式的,
LazyStaticSingleton lazyStaticSingleton = new LazyStaticSingleton();
看似是饿汉式的,但是只有调用getLazyStaticSingleton
时才会进行初始化,线程安全由ClassLoad
保证,不用思考怎么加锁
前面几种方式实现单例的方式虽然各有优缺点,但是基本实现了单例线程安全的要求。但是总有人看不惯单例模式勤俭节约的作用,对它进行攻击。对它进行攻击无非就是创建不只一个类,java
中创建对象的方式有new
、clone
、序列化、反射。构造函数私有化不可能通过new创建对象、同时单例类没有实现Cloneable
接口无法通过clone
方法创建对象,那剩下的攻击只有反射攻击和序列化攻击了
反射攻击:
public class ReflectAttackTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//静态内部类
LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
//通过反射创建LazyStaticSingleton
Constructor constructor = LazyStaticSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazyStaticSingleton lazyStaticSingleton1 = constructor.newInstance();
//打印结果为false,说明又创建了一个新对象
System.out.println(lazyStaticSingleton == lazyStaticSingleton1);
//synchronize
LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
Constructor lazySynchronizeSingletonConstructor = LazySynchronizeSingleton.class.getDeclaredConstructor();
lazySynchronizeSingletonConstructor.setAccessible(true);
LazySynchronizeSingleton lazySynchronizeSingleton1 = lazySynchronizeSingletonConstructor.newInstance();
System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);
//lock
LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
Constructor lazyLockSingletonConstructor = LazyLockSingleton.class.getConstructor();
lazyLockSingletonConstructor.setAccessible(true);
LazyLockSingleton lazyLockSingleton1 = lazyLockSingletonConstructor.newInstance();
System.out.println(lazyLockSingleton == lazyLockSingleton1);
//双重锁检查
LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
Constructor lazyDoubleCheckSingletonConstructor = LazyDoubleCheckSingleton.class.getConstructor();
lazyDoubleCheckSingletonConstructor.setAccessible(true);
LazyDoubleCheckSingleton lazyDoubleCheckSingleton1 = lazyDoubleCheckSingletonConstructor.newInstance();
System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton1);
}
}
基于静态内部类和基于synchronize
加锁创建单例对象的方式都可以通过反射的方式创建新对象,存在反射攻击,其余几种创建单例对象的方式使用反射创建新对象将会报错
反序列化攻击:
public class SerializableAttackTest {
public static void main(String[] args) {
//懒汉式
HungrySingleton hungrySingleton = HungrySingleton.getHungrySingleton();
//序列化
byte[] serialize = SerializationUtils.serialize(hungrySingleton);
//反序列化
HungrySingleton hungrySingleton1 = SerializationUtils.deserialize(serialize);
System.out.println(hungrySingleton == hungrySingleton1);
//双重锁
LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
byte[] serialize1 = SerializationUtils.serialize(lazyDoubleCheckSingleton);
LazyDoubleCheckSingleton lazyDoubleCheckSingleton11 = SerializationUtils.deserialize(serialize1);
System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton11);
//lock
LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
byte[] serialize2 = SerializationUtils.serialize(lazyLockSingleton);
LazyLockSingleton lazyLockSingleton1 = SerializationUtils.deserialize(serialize2);
System.out.println(lazyLockSingleton == lazyLockSingleton1);
//synchronie
LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
byte[] serialize3 = SerializationUtils.serialize(lazySynchronizeSingleton);
LazySynchronizeSingleton lazySynchronizeSingleton1 = SerializationUtils.deserialize(serialize3);
System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);
//静态内部类
LazyStaticSingleton lazyStaticSingleton = LazyStaticSingleton.getLazyStaticSingleton();
byte[] serialize4 = SerializationUtils.serialize(lazySynchronizeSingleton);
LazyStaticSingleton lazyStaticSingleton1 = SerializationUtils.deserialize(serialize4);
System.out.println(lazyStaticSingleton == lazyStaticSingleton1);
}
}
打印结果都为false,都存在反序列化攻击
对于反射攻击我们只能选择放弃使用存在这种缺陷的单例创建方式,但是反序列话攻击还是可以在抢救一下的,抢救姿势如下:
private Object readResolve() {
return lazySynchronizeSingleton;
}
添加readResolve
方法并返回创建的单例对象,至于抢救的原理,可以通过跟进SerializationUtils.deserialize
的代码可知。上述实现单例对象的方式既要考虑线程安全、又要考虑攻击,而通过枚举创建单例对象完全不用担心这些问题
- 枚举
代码实现也相当优美,总共才public enum EnumSingleton { INSTANCE; public static EnumSingleton getEnumSingleton() { return INSTANCE; } }
8
行代码
实现原理:枚举类的域(field)其实是相应的enum类型的一个实例对象
可以参考:implementing-singleton-with-an-enum-in-java
单例模式的优点
- 只创建了一个实例,节省内存开销
- 减少了系统的性能开销,创建对象回收对象对性能都有一定的影响
- 避免对资源的多重占用
- 在系统设置全局的访问点,优化和共享资源优化
总结一下就是节约资源、提升性能
单例模式的缺点
- 不适用于变化的对象
- 单例模式中没有抽象层,扩展有困难
- 与单一原则冲突。一个类应该只实现一个逻辑,而不关心它是否单例,是不是单例应该由业务决定
单例模式的应用场景
-
Spring IOC
默认使用单例模式创建bean
- 创建对象需要消耗的资源过多时
- 需要定义大量的静态常量和静态方法的环境,比如工具类【感觉是最常见应用场景】
小结
总共介绍了六种正确创建单例对象的方式,推荐使用饿汉式创建单例对象的方式,如果对资源使用有要求,则推荐使用静态内部类【注意反序列化攻击】,其他方式在保证线程安全的同时对性能将会有影响。枚举类其实是非常不错的,线程安全、不存在反射攻击和反序列化攻击,但是感觉这种创建单例方式应用较少,公司代码中使用的是双重锁检查和静态内部类【存在反序列化攻击】创建单例方式,甚至之前出去面试时面试官让写一个单例,我使用的是枚举方式,面试官都不知道有这种方式
最后附:完整例子代码+测试代码
欢迎fork与star