看公司代码的时候发现项目中单例模式应用挺多的,而且发现的两处单例模式用的仍是不一样的方式实现的,那么单例模式到底有几种写法呢?单例模式看似很简单,可是实际写起来却问题多多java
确保某个类只有一个实例,并且自行实例化并向整个系统提供这个实例,而且有两种建立方式,一种是饿汉式建立,另一种是懒汉式建立git
饿汉式建立就是在类加载时就已建立好对象,而不是在须要时在建立对象github
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
/**
* 私有构造函数,不能被外部所访问
*/
private HungrySingleton() {}
/**
* 返回单例对象
* */
public static HungrySingleton getHungrySingleton() {
return hungrySingleton;
}
}
复制代码
说明:面试
getHungrySingleton
方法获取对象HungrySingleton
对象已经建立完成【在类加载时建立】缺点:安全
getHungrySingleton
一直没有被使用到,有点浪费资源优势:bash
ClassLoad
保证线程安全懒汉式建立就是在第一次须要该对象时在建立函数
存在错误的懒汉式建立单例对象
根据定义很容易在上面饿汉式的基础上进行修改工具
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<LazyStaticSingleton> constructor = LazyStaticSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazyStaticSingleton lazyStaticSingleton1 = constructor.newInstance();
//打印结果为false,说明又建立了一个新对象
System.out.println(lazyStaticSingleton == lazyStaticSingleton1);
//synchronize
LazySynchronizeSingleton lazySynchronizeSingleton = LazySynchronizeSingleton.getLazySynchronizeSingleton();
Constructor<LazySynchronizeSingleton> lazySynchronizeSingletonConstructor = LazySynchronizeSingleton.class.getDeclaredConstructor();
lazySynchronizeSingletonConstructor.setAccessible(true);
LazySynchronizeSingleton lazySynchronizeSingleton1 = lazySynchronizeSingletonConstructor.newInstance();
System.out.println(lazySynchronizeSingleton == lazySynchronizeSingleton1);
//lock
LazyLockSingleton lazyLockSingleton = LazyLockSingleton.getLazyLockSingleton();
Constructor<LazyLockSingleton> lazyLockSingletonConstructor = LazyLockSingleton.class.getConstructor();
lazyLockSingletonConstructor.setAccessible(true);
LazyLockSingleton lazyLockSingleton1 = lazyLockSingletonConstructor.newInstance();
System.out.println(lazyLockSingleton == lazyLockSingleton1);
//双重锁检查
LazyDoubleCheckSingleton lazyDoubleCheckSingleton = LazyDoubleCheckSingleton.getLazyDoubleCheckSingleton();
Constructor<LazyDoubleCheckSingleton> lazyDoubleCheckSingletonConstructor = LazyDoubleCheckSingleton.class.getConstructor();
lazyDoubleCheckSingletonConstructor.setAccessible(true);
LazyDoubleCheckSingleton lazyDoubleCheckSingleton1 = lazyDoubleCheckSingletonConstructor.newInstance();
System.out.println(lazyDoubleCheckSingleton == lazyDoubleCheckSingleton1);
}
}
复制代码
基于静态内部类和基于synchronize
加锁建立单例对象的方式均可以经过反射的方式建立新对象,存在反射攻击,其他几种建立单例对象的方式使用反射建立新对象将会报错。针对存在的反射攻击根据网上提供的思路在抢救一下,抢救姿式以下:
private LazySynchronizeSingleton() {
//flag为线程间共享,进行加锁控制
synchronized (LazySynchronizeSingleton.class) {
if (flag == false) {
flag = !flag;
} else {
throw new RuntimeException("单例模式被攻击");
}
}
}
复制代码
构造函数只能调用一次,调用第二次将抛出异常,经过flag
来判断构造函数是否已经被调用过一次了。可是咱们仍能够经过反射修改flag
的值:
//调用反射前将flag设置为false
Field flagField = lazySynchronizeSingleton.getClass().getDeclaredField("flag");
flagField.setAccessible(true);
flagField.set(lazySynchronizeSingleton, false);
复制代码
抢救失败,你可能想经过final
修饰禁止修改,可是反射能够先去除final
,在加上final
修改值,对于反射攻击,无力回天,只能选择不适用存在反射攻击的单例建立方式
反序列化攻击:
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
行代public class EnumAttackTest {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
EnumSingleton enumSingleton = EnumSingleton.getEnumSingleton();
//序列化攻击
byte[] serialize4 = SerializationUtils.serialize(enumSingleton);
EnumSingleton enumSingleton2 = SerializationUtils.deserialize(serialize4);
System.out.println(enumSingleton == enumSingleton2);
//反射攻击
Constructor<EnumSingleton> enumSingletonConstructor = EnumSingleton.class.getConstructor();
enumSingletonConstructor.setAccessible(true);
EnumSingleton enumSingleton1 = enumSingletonConstructor.newInstance();
System.out.println(enumSingleton == enumSingleton1);
}
}
复制代码
反射攻击将会抛出异常,序列化攻击对它无效,打印结果为true
,用枚举建立单例对象真的是无懈可击总结一下就是节约资源、提高性能
Spring IOC
默认使用单例模式建立bean
总共介绍了六种正确建立单例对象的方式,推荐使用饿汉式建立单例对象的方式,若是对资源使用有要求,则推荐使用静态内部类【注意反序列化攻击】,其余方式在保证线程安全的同时对性能将会有影响。枚举类实际上是很是不错的,线程安全、不存在反射攻击和反序列化攻击,可是感受这种建立单例方式应用较少,公司代码中使用的是双重锁检查和静态内部类【存在反序列化攻击】建立单例方式,甚至以前出去面试时面试官让写一个单例,我使用的是枚举方式,面试官都不知道有这种方式
最后附:完整例子代码+测试代码
欢迎fork与star