单例模式可以说是设计模式中最简单和最基础的一种设计模式了,但是想写对单例模式真的很难,下面看下单例模式的几种写法及其存在的问题分析。
饿汉式
饿汉式是最常见也是最不需要考虑太多的单例模式,因为本就是线程安全的,饿汉式也就是在类被加载的时候就创建实例对象,饿汉式的代码如下:
public class Singleton {
private static Singleton singleton = new Singleton();
/**
* 私有构造函数
*/
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
优点:是线程安全的
缺点:在类被加载的时候对象就会被创建,也就是说不管是否使用到该对象此对象都会被创建,浪费内存空间,而且如果对象的创建比较复杂,耗时比较久,这也会导致整个加载时间变长
懒汉式
代码如下:
public class Singleton {
private static Singleton singleton = null;
/**
* 私有构造函数
*/
private Singleton() {
}
public static Singleton getInstance() {
/*存在线程安全问题
*当线程一执行了if (singleton == null) 没执行singleton = new Singleton();
*此时线程二执行if (singleton == null) 这种情况就会导致线程一和线程二拿到不同的对象
*/
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
优点:单线程下性能比较好
缺点:多线程环境下会存在线程安全问题,见上面的注释
DCL(双重检查锁)
public class Singleton {
private static Singleton singleton = null;
/**
* 私有构造函数
*/
private Singleton() {
}
public static Singleton getInstance() {
if (null == singleton) {
synchronized (Singleton.class) {
if (null == singleton) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
我最开始时认为这种形式已经是最终的形式了,首先是判断对象是否为空,如果为空,则先加锁之后再判断一次是否为空,如果还为空,则再来执行实例化对象操作,一切看起来似乎没雨任何问题,直到看过《并发编程的艺术》这一本书之后看到有问题。具体问题如下
singleton = new Singleton();可以分解为如下三行代码:
memory = allocate(); //1、分配存储空间
ctorInstance(memory); //2、初始化对象
singleton = memory; //3、设置singleton指向刚分配的内存地址
上面步骤3设置singleton的值和步骤2初始化对象可能会被重排序,这种重排序是真实存在的。如果3被重排序到2之前,此时其他线程刚好判断到singleton!=null,会直接使用一个没有进行初始化的对象。
知道了问题存在的原因,解决办法也很简单。有如下两种解决办法:
1、不允许2 和 3重排序
2、允许2 和 3重排序,但是不允许其他线程“看到”这个重排序
基于volatile的解决方案(即不允许2 和 3 重排序)
public class Singleton {
private static volatile Singleton singleton = null;
/**
* 私有构造函数
*/
private Singleton() {
}
public static Singleton getInstance() {
if (null == singleton) {
synchronized (Singleton.class) {
if (null == singleton) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
只需要把singleton设置为volatile,注意这个解决方案需要JDK5或者更高版本,因为从JDK5开始使用心得JSR-133内存模型规范,这个规范增强了volatile的语义。JMM针对编译器制定的volatile重排序规则表。参考《并发编程的艺术》一书。
JMM会在每个volatile写操作的前面插入一个StoreStore屏障
在每个volatile些操作的后面插入一个StoreLoad屏障
在每个volatile读操作的后面插入一个LoadLoad屏障
在每个volatile读操作的后面插入一个LoadStore屏障
从而阻止了2和3的重排序。
基于类初始化的解决方案(允许2 和 3重排序,但是不允许其他线程“看到”这个重排序)
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
public static Singleton singleton = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
但是上面的单例模式都不是完美的,主要有以下两个原因。
1、反射攻击
首先我们来验证DCL的反射攻击
public class SingletonTest {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
Class<Singleton> singletonClass = Singleton.class;
//获取类的构造器
Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
//把构造器私有权限放开
constructor.setAccessible(true);
//调用反射获取对象
Singleton instance = constructor.newInstance();
//正常的获取实例方式 正常的方式放在反射创建实例后面,这样当反射创建成功后,单例对象中的引用其实还是空的,反射攻击才能成功
Singleton instance1 = Singleton.getInstance();
System.out.println("instance1 = " + instance1);
System.out.println("instance = " + instance);
}
}
输出的结果如下,发现居然是不同的对象。
2、序列化攻击
需要序列化,需要实现接口 Serializable
public class SingletonTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton singleton = Singleton.getInstance();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("singleton_file"));
//序列化写操作
objectOutputStream.writeObject(singleton);
//读取序列化的文件并进行反序列化操作
File file = new File("singleton_file");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
Singleton newSingleton = (Singleton) objectInputStream.readObject();
System.out.println(singleton);
System.out.println(newSingleton);
System.out.println(singleton == newSingleton);
}
}
输出结果如下:
结果果然不一样,这种反序列化攻击其实解决方式也简单,重写反序列化时要调用的 readObject 方法即可
如下:至于为什么加一个就可以,和接口 Serializable 的作用 ,放到后续单独开一篇序列化和反序列化的文章进行说明。
private Object readResolve(){
return instance;
}
真正安全的单例模式:枚举,枚举相关的可以看下这篇文章
public enum EnumSingleton {
/**
* 实例对象
*/
INSTANCE;
public void sayHello() {
System.out.println("Hello World!");
}
}
反编译看下如下:
public enum EnumSingleton {
INSTANCE;
// $FF: synthetic field
private static final EnumSingleton[] $VALUES = new EnumSingleton[]{INSTANCE};
public void sayHello() {
System.out.println("Hello World!");
}
}
我们试下枚举反射时会出现什么问题:
public class EnumSingletonTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<EnumSingleton> singleTonEnumClass = EnumSingleton.class;
//获取构造函数
Constructor<EnumSingleton> declaredConstructor = singleTonEnumClass.getDeclaredConstructor();
//设置访问权限
declaredConstructor.setAccessible(true);
EnumSingleton singleTonEnum = declaredConstructor.newInstance();
EnumSingleton instance = EnumSingleton.INSTANCE;
System.out.println("instance = " + instance);
System.out.println("singleTonEnum = " + singleTonEnum);
}
}
结果如下:
发现抛出异常了,究竟是什么原因导致抛出异常?提示没有 com.Ycb.singleton.EnumSingleton.<init>()
根据堆栈可以看出实在Class的getConstructor0()方法跑出来的。Class的代码如下
private Constructor<T> getConstructor0(Class<?>[] parameterTypes,
int which) throws NoSuchMethodException
{
Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));
for (Constructor<T> constructor : constructors) {
if (arrayContentsEq(parameterTypes,
constructor.getParameterTypes())) {
return getReflectionFactory().copyConstructor(constructor);
}
}
throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes));
}
看下EnumSingleton的字节码,发现<init>方法带有两个参数,第一个参数是String,第二个参数是Int类型的,并没有无参的<init>方法
反射时获取有参的构造函数。
//获取构造函数
Constructor<EnumSingleton> declaredConstructor = singleTonEnumClass.getDeclaredConstructor(String.class, int.class);
执行结果如下:发现还是报错,但是报错变了。
提示14行报错,14行是 EnumSingleton singleTonEnum = declaredConstructor.newInstance();接着看下 Constructor 的newInstance()方法。
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
通过看 Constructor 的newInstance()方法,如果是Enum时直接抛出异常了,不允许通过反射来创建,这才是使用 enum 创建单例才可以说是真正安全的原因!