你写的单例真的安全吗?

单例模式

单例模式的实现方案有两种:饿汉式、懒汉式。

网上很多写法都存在一些问题。

饿汉式

饥饿法则,优先创建实例,需要时直接返回。

优点是代码编写简单,缺点是没有达到懒加载的效果,浪费资源。

public class Single {
	private final static Single SINGLE = new Single();
	private Single(){}

	public static Single getInstance(){
		return SINGLE;
	}
}

懒汉式

顾名思义,默认不实例化,需要时再实例化。

public class Single {
	private static Single single;
	private Single(){}

	public static Single getInstance(){
		if (single == null) {
			single = new Single();
		}
		return single;
	}
}

这是线程不安全的,并发的情况下,还是会创建多个实例。

双重加锁检查

通过加锁的方式进行双重检查,这样就是线程安全的,即使并发也只会创建一个实例。

public class Single {
	private static Single single;
	private Single(){}

	public static Single getInstance(){
		if (single == null) {
			synchronized (Single.class) {
				if (single == null) {
					single = new Single();
				}
			}
		}
		return single;
	}
}

反射攻击

实际上,上面两种方式并没有达到真正的“单例”。

因为都忽略了一个很严重的问题:反射可以调用私有构造器

例如,下面这段代码可以任意实例化单例类:

public class Client {
	public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
		//获取Single的构造器
		Constructor<Single> constructor = Single.class.getDeclaredConstructor();
		//取消访问检查
		constructor.setAccessible(true);
		Single s1 = constructor.newInstance();
		Single s2 = constructor.newInstance();
		System.out.println(s1);
		System.out.println(s2);
	}
}
//输出:
unit1.item3.Single@511d50c0
unit1.item3.Single@60e53b93

改造单例

弄清楚原因之后,进行改造就很简单了,只需要在私有构造器内稍作判断即可。

public class Single {
	private static Single single;
	private Single(){
		synchronized (Single.class){
			if (single != null) {
				throw new RuntimeException("拒绝再次实例.");
			}
			single = this;
		}
	}

	public static Single getInstance(){
		if (single == null) {
			synchronized (Single.class) {
				if (single == null) {
					single = new Single();
				}
			}
		}
		return single;
	}
}

序列化对单例的破坏

当单例类允许被序列化后,如果不做处理,单例模式也会被破坏。

public class Client {
	public static void main(String[] args) throws Exception {
		//获取实例
		Single s1 = Single.getInstance();

		//序列化
		File file = new File("/Users/panchanghe/temp/Single.obj");
		OutputStream outputStream = new FileOutputStream(file);
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
		objectOutputStream.writeObject(s1);
		objectOutputStream.flush();
		objectOutputStream.close();
		outputStream.close();

		//反序列化
		InputStream inputStream = new FileInputStream(file);
		ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
		Single s2 = (Single) objectInputStream.readObject();
		objectInputStream.close();
		inputStream.close();
		System.out.println(s1);
		System.out.println(s2);
	}
}

输出如下:

unit1.a.Single@6f94fa3e
unit1.a.Single@4e50df2e

反序列化后得到的是一个新的实例。

任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。

要想解决也很简单,编写readResolve()方法,如下:

//防止序列化对单例的破坏
private Object readResolve() {
	return single;
}

枚举实现单例

单元素的枚举类型已经成为实现Singleton的最佳方法。——《Effective Java》

枚举实现单例非常简单,即可以防止反射攻击,还可以防止序列化破坏,因为Enum重写了readObject方法,

public enum  EnumSingle {
	INSTANCE;

	public static EnumSingle getInstance(){
		return INSTANCE;
	}

	//属性、方法照常写...
	public void func(){
		System.out.println("func....");
	}
}

反射攻击验证

Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor();

如上方法去获取构造器会报错:NoSuchMethodException

一旦将类声明为枚举类型,该类就自动继承了Enum,查看Enum的源码,会发现其只有一个构造器:

protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

通过获取父类的构造器来实例化,测试代码修改如下:

Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingle enumSingle = constructor.newInstance("instance",0);

输出如下:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at unit1.item3.Client.main(Client.java:18)

反射时抛出异常,不能反射创建枚举对象。

查看newInstance()源码,发现如下:

在这里插入图片描述

Java反射时会进行检查,如果是枚举类型,会抛出异常,不允许反射实例化。

序列化破坏验证

public class Client {
	public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, IOException, ClassNotFoundException {
		//获取实例
		EnumSingle s1 = EnumSingle.getInstance();

		//序列化
		File file = new File("/Users/panchanghe/temp/Single.obj");
		OutputStream outputStream = new FileOutputStream(file);
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
		objectOutputStream.writeObject(s1);
		objectOutputStream.flush();
		objectOutputStream.close();
		outputStream.close();

		//反序列化
		InputStream inputStream = new FileInputStream(file);
		ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
		EnumSingle s2 = (EnumSingle) objectInputStream.readObject();
		objectInputStream.close();
		inputStream.close();
		System.out.println(s1);
		System.out.println(s2);
		System.out.println(s1 == s2);
	}
}

输出如下:

INSTANCE
INSTANCE
true

使用枚举来实现,即使是序列化也不会破坏其单例模式。

发布了100 篇原创文章 · 获赞 23 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_32099833/article/details/103604486