文章目录
为什么要使用单例模式
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。
- 由于new操作的次数减少,所以系统内存的使用评率也会降低,这将减少GC压力,缩短GC停顿时间。
懒汉模式与饿汉模式
饿汉模式及代码实现
饿汉模式通过static修饰符修饰,以及构造函数的私有化实现单例模式
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
由于使用了static修饰符,在类加载的时候就完成了初始化,所以饿汉模式是线程安全的,但就因为类加载的时候就完成了初始化,没有懒加载的效果所以会浪费内存
当Java程序需要使用某个类时,如果该类还未被加载到内存中,JVM会通过加载、连接(验证、准备和解析)、初始化三个步骤来对该类进行初始化。
类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:
1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
2)如果类中存在初始化语句,就依次执行这些初始化语句。
懒汉模式及代码实现
懒汉模式在调用的时候才进行new的操作,从而节约了内存,懒汉式总共有三种实现方式,即
- 双重检查锁
- 静态内部类
- 枚举
双重检查锁方式
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (null == singleton) {
return new Singleton();
}
return singleton;
}
}
上述的代码有一个致命的问题,在并发操作中,多个线程同时调用获得实例的方法时,极有可能出现new多次取得不同实例的问题。
为了解决这个问题,其实最简单的办法就是加锁,加在方法上是最直接有效的,如下
public static synchronized Singleton getInstance() {
if (null == singleton) {
return new Singleton();
}
return singleton;
}
但是会产生效率问题,即new的操作只会进行一次,而加在方法上,同时只能有一个线程来获取实例。所以可以改为下面的方式
public static Singleton getInstance() {
if (null == singleton) {
synchronized(Singleton.class) {
if (null == singleton) {
return new Singleton();
}
}
}
return singleton;
}
这种方式又称为双重检查锁。到目前为止看似没有问题,其实还是有潜在的隐患,即指令重排,因为new操作并不是一个原子性的操作,它分为一下三个步骤
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
而指令重排之后变为
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
这样通过getInstance取得实例可能只分配了内存地址,而并没有初始化完成即调用属性方法时会抛异常出来(大概是NoSuchMethodException…懒了不跑了)。
所以完整版双重检查锁模式如下
public class Singleton {
// 使用volatile关键字,即内存屏障功能
// 相当于禁止使用CPU高速缓存,从而使每次修改前都要同步到主内存中避免了乱序执行的可能保证了并发操作的可见性
private static volatile Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (null == singleton) {
synchronized(Singleton.class) {
if (null == singleton) {
return new Singleton();
}
}
}
return singleton;
}
}
静态内部类方式
静态内部类方式
public class StaticSingleton {
// 由于StaticSingleton没有静态成员,所以体现出了懒汉式思想
private StaticSingleton() {
}
// JVM在类加载的时候是互斥的,所以是线程安全的
private static class SingletonBuilder {
private static StaticSingleton staticSingleton = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return SingletonBuilder.staticSingleton;
}
}
枚举单例写法(最简单最安全)
public enum EnumSingleton {
INSTANCE;
}
如何破坏一个单例
反射攻击
直接上代码:
public static void reflectionAttack() throws Exception {
//通过反射,获取单例类的私有构造器
Constructor constructor = DoubleCheckLockSingleton.class.getDeclaredConstructor();
//设置私有成员的暴力破解
constructor.setAccessible(true);
// 通过反射去创建单例类的多个不同的实例
DoubleCheckLockSingleton s1 = (DoubleCheckLockSingleton) constructor.newInstance();
// 通过反射去创建单例类的多个不同的实例
DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton) constructor.newInstance();
s1.tellEveryone();
s2.tellEveryone();
System.out.println(s1 == s2);
}
执行结果如下:
This is a DoubleCheckLockSingleton 1368884364
This is a DoubleCheckLockSingleton 401625763
false
这种方法非常简单暴力,通过反射侵入单例类的私有构造方法并强制执行,使之产生多个不同的实例,
这样单例就被破坏了。要防御反射攻击,只能在单例构造方法中检测instance是否为null,如果已不为
null,就抛出异常。显然双重检查锁实现无法做这种检查,静态内部类实现则是可以的。
注意,不能在单例类中添加类初始化的标记位或计数值(比如 boolean flag 、 int count )来
防御此类攻击,因为通过反射仍然可以随意修改它们的值。
序列化攻击
这种攻击方式只对实现了Serializable接口的单例有效,但偏偏有些单例就是必须序列化的。现在假设
DoubleCheckLockSingleton类已经实现了该接口,上代码:
public static void serializationAttack() throws Exception {
// 对象序列化流去对对象进行操作
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serFile"));
//通过单例代码获取一个对象
DoubleCheckLockSingleton s1 = DoubleCheckLockSingleton.getInstance();
//将单例对象,通过序列化流,序列化到文件中
outputStream.writeObject(s1);
// 通过序列化流,将文件中序列化的对象信息读取到内存中
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("serFile")));
//通过序列化流,去创建对象
DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton) inputStream.readObject();
s1.tellEveryone();
s2.tellEveryone();
System.out.println(s1 == s2);
}
执行结果如下:
This is a DoubleCheckLockSingleton 777874839
This is a DoubleCheckLockSingleton 254413710
false
为什么会发生这种事?长话短说,在 ObjectInputStream.readObject() 方法执行时,其内部方法
readOrdinaryObject()中有这样一句话:
// desc是类描述符
obj = desc.isInstantiable() ? desc.newInstance() : null
也就是说,如果一个实现了Serializable/Externalizable接口的类可以在运行时实例化,那么就调用
newInstance()方法,使用其默认构造方法反射创建新的对象实例,自然也就破坏了单例性。要防御序
列化攻击,就得将instance声明为transient,并且在单例中加入以下语句:
private Object readResolve() {
return instance;
}
这是因为在上述readOrdinaryObject()方法中,会通过卫语句 desc.hasReadResolveMethod() 检查类
中是否存在名为readResolve()的方法,如果有,就执行 desc.invokeReadResolve(obj) 调用该方
法。readResolve()会用自定义的反序列化逻辑覆盖默认实现,因此强制它返回instance本身,就可以防
止产生新的实例。
枚举单例的防御机制
对反射的防御
我们直接将上述reflectionAttack()方法中的双重检查锁方式的类名改成EnumSingleton并执行,会抛出NoSuchMethodException。
这是因为所有Java枚举都隐式继承自Enum抽象类,而Enum抽象类根本没有无参构造方法,只有如下一
个构造方法:
那么我们就改成获取这个有参构造方法,即:
Constructor constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
结果还是会抛出异常
来到Constructor.newInstance()方法中,有如下语句:
可见,JDK反射机制内部完全禁止了用反射创建枚举实例的可能性。
对序列化的防御
如果将serializationAttack()方法中的攻击目标换成EnumSingleton,那么我们就会发现s1和s2实际上是
同一个实例,最终会打印出true。这是因为ObjectInputStream类中,对枚举类型有一个专门的
readEnum()方法来处理,其简要流程如下:
- 通过类描述符取得枚举单例类型
- 取得枚举单例中的枚举值的名字(这里是INSTANCE)
- 调用Enum.valueOf()方法,根据枚举类型和枚举值的名字,获得最终的单例
这种处理方法与readResolve()方法大同小异,都是以绕过反射直接获取单例为目标。不同的是,枚举对
序列化的防御仍然是JDK内部实现的。
综上所述,枚举单例确实是目前最好的单例实现了,不仅写法非常简单,并且JDK能够保证其安全性,
不需要我们做额外的工作。