安全的单例模式
单例模式的形式与安全性单例模式的应用场景绝大多数玩过Java的同学应该都有了解过单例模式,而为什么我们需要使用单例模式?它的应用场景是什么呢?想到单例模式可能会想到数据库连接池,或者在Spring框架中的ApplicationContext上下文等等,而它们为什么要做成单例?如果不是单例有什么问题呢?我们先明白单例模式的概念:确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。这里说的
安全的单例模式
单例模式的形式与安全性
单例模式的应用场景
绝大多数玩过Java的同学应该都有了解过单例模式,而为什么我们需要使用单例模式?它的应用场景是什么呢?
想到单例模式可能会想到数据库连接池,或者在Spring框架中的ApplicationContext上下文等等,而它们为什么要做成单例?如果不是单例有什么问题呢?
我们先明白单例模式的概念:确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。这里说的任何情况一般指的是虚拟机管理的内存中唯一。
为什么连接池要是单例的呢?我们要知道计算机的资源是有限的,而我们之所有用池的概念抽象这些连接,最重要的是我们能对这些连接进行复用、管理。如果每一个请求进来我都让它拥有创建连接池的权利,那么我计算机的资源是不是在接受不到几个请求以后就已经耗尽了?而复用、管理的想法也难以得到实现,也就是我希望能节省我的资源,这是其一。
另一个问题为什么要对ApplicationContext进行单例呢,如果对于多个线程来说我有多个全局上下文,那么我如何保持全局的一致性呢?如何保持线程安全呢?当然这里并不是说单例就一定能保持线程安全,线程安全与否取决于是否有“状态”,示例的话就是Spring中管理的单例bean。
单例的实现方式
稍微了解过单例模式的人应该都知道,我们最常用的实现单例的方式有饿汉式和懒汉式,它们的区别的主要创建的时机有所不同,顾名思义,饿汉式的话仿佛一个几天没吃饭的人,一上来先不管你用不用得上,直接干就完事了。而懒汉式则是你催我我不动,我就是玩,等真正被需要的时候再去创建一个单例对象。虽然这么说他们但是其实优劣各异,饿汉式主要是不被需要的时候就被创建,抬杠的话就是它浪费资源,而懒汉式的话就是在用的时候才创建,硬杠的话就是在我需要的时候又不能马上给我,所以我还得等。其实总的来说就是时间和空间之间的取舍,这些都是小问题,所以不展开讲。
下面写几个较经典的饿汉式单例和懒汉式单例
饿汉式
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
可以看出这个单例对象的创建很简单,由于是静态变量,所以它在类加载的阶段就已经被初始化了,它的构造方法以及参数属性都是私有的,也就是屏蔽了外界通过构造函数的直接创建,而只暴露出了唯一的获取单例对象接口,从而保证了这个单例对象的唯一性。
懒汉式
public class Singleton {
private volatile Singleton singleton;
private Object mutex;
private Singleton(){}
public Singleton getSingleton(){
if (singleton == null){
synchronized (mutex){
if (singleton == null){
singleton = new Singleton2();
}
}
}
return singleton;
}
}
可以看到,我们一开始并没有初始化,而是在被调用获取实例方法时候去检查,上面这种属于双重校验的实现单例,由于做了双重判断,因此也比直接上来就用synchronized锁住整个方法要效率更高,还有一个值得注意的是这里的单例是用volatile修饰的,了解volatile过的可能知道它最大的特点就是可见性了,但是在这里并不是为了保证他的可见性,而是防止重排序,我们知道cpu、编译器其实都会对代码进行优化,从而来保证效率。而初始化一个对象其实不是一个原子操作的过程,概括的说是分为三个步骤,分别是为对象分配内存、初始化对象、设置一个变量指向刚分配的内存地址。而如果没有重排序,可能先把Singleton直接指向了内存地址,而没有初始化对象,从而在并发情况下,线程进来判断发现变量是有指向的就直接返回了,而此时的对象并没有被初始化,这就是由于重排序带来的问题了。
而使用synchronized修饰代码是为了只能有一个线程进来初始化,从而避免被初始化多次,而造成的线程不安全问题。由于synchronized不是本篇的主要内容,所以不做过多阐述了。
当然,上面举得是比较经典的几个例子,当然实现的方式还有多种多样的,这里就不一一列举了。
反射破坏单例
接来下进入我们的主题,既然单例是为了保证其在虚拟机内存的唯一性,那么在什么时候会破坏了单例模式呢?
我们可以思考一下,如果创建一个对象?最显示的方法就是通过声明式的new出来,然后还有一种容易想到的就是反射,反射大法好,在印象里就没有反射干不了的东西,即使想Unsafe这种虚拟机都小心翼翼保护的对象都能通过反射拿到,更何况我们自己创建的呢?
接下来我们就尝试用反射去破坏我们的单例环境
public class DestructionSingleton {
private static volatile Singleton singleton;
private Singleton(){}
public static Singleton getSingleton(){
if (singleton == null){
synchronized (Singleton.class){
if (singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
执行结果:
可以看出来他们并不是一个对象,我们通过获取他的构造方法并进行强制访问,从而创建出一个新的对象。因此,我们可以通过反射来直接破坏我们的单例模式。
不过到这里我们可以思考一下,既然你是从构造函数来强制让我创建,那么我就在构造函数上下手脚,让你即使调用构造函数也没法真正的执行它!
public class Singleton2 {
private static volatile Singleton2 singleton;
private Singleton2(){
if (singleton != null){
throw new RuntimeException("不允许创建多个实例");
}
}
public static Singleton2 getSingleton(){
if (singleton == null){
synchronized (Singleton2.class){
if (singleton == null){
singleton = new Singleton2();
}
}
}
return singleton;
}
}
执行结果:
我们又进一步的保护了单例模式。
反序列化破坏单例
再往后想一步,除了反射还有什么方法能够创建一个对象?了解过原型模式应该会想到一种骚操作,就是通过序列化来创建一个对象,毕竟我们使用RPC或者持久化一个对象的时候可都是通过序列化呀,所以我们尝试用反序列化的方式创建一个对象。
简单说一下什么是序列化和反序列化:序列化就是把内存中的状态通过转换成字节码的形式,从而转换一个IO流,写入其他地方(磁盘、网络),内存中的对象也会被持久化下来。
反序列化就是将已经持久化的字节码内容转为IO流,通过IO流的读取,进而将读取的内容转换为Java对象,在转换过程中会重新创建对象。
public class Singleton implements Serializable {
private static Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
public class SerializeSingleton {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
FileOutputStream fos = new FileOutputStream("singleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton singleton2 = (Singleton) ois.readObject();
ois.close();
System.out.println(singleton2);
}
}
值得注意的是,我们在这个类上要实现Serializable接口。
执行结果:
很直观的可以看到,我们成功的通过反序列化的方式破坏了单例模式。那么,我们究竟要如何保证在能够序列化的情况下也能实现单例模式?这里先斩后奏一下,下面是优化方法
public class Singleton implements Serializable {
private static Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
public Object readResolve(){
return singleton;
}
}
我们仅仅在代码中加入一个readResolve方法,并把已经创建好的实例当做返回参数,就能保持单例模式了?这究竟是怎么做到的呢,下面我们逐步分析
首先,我们从ObjectInputStream类的readObject方法开始,相关代码如下:
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
可以看到在readObject方法中又调取了readObject0的方法,继续跟进去
由于篇幅过长,我们截取最关键的一行代码
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
通过判断这个序列化是属于Object的,所以继续跟进这行代码
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
在这行代码中值得注意的是这一行代码
obj = desc.isInstantiable() ? desc.newInstance() : null;
继续跟进到isInstantiable()方法中。
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
可以看到取决定性因素的哪一行cons != null 其实就是判断一下这个类的构造方法是否为空,如果构造方法不为空就返回true。这意味着只要有无参构造方法就会被实例化。
我们再回到readOrdinaryObject方法中,判断无参构造方法是否存在以后,有调用了hasReadResolveMethod方法,跟进去看一下
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
readResolveMethod是什么呢,我们全局搜一下。
在私有方法ObjectStreamClass中给readResolveMethod赋值了,代码入下:
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
上面的逻辑其实就是通过逻辑找到一个无参的readResolve()方法,并且保存下来,现在回到ObjectInpStream的readOrdinaryObject()方法继续往下看,如果readResolve()方法存在则调用invokeReadResolve()方法。
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
requireInitialized();
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException) th;
} else {
throwMiscException(th);
throw new InternalError(th); // never reached
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
我们可以看到,在invokeReadResolve()方法中反射调用了readResolveMethod方法。
这里需要理清一个误区,我们在获取序列化后的obj是在readSerialData方法,而使用readResolveMethod其实是一个钩子方法,用来做兜底的,意思就是如果你想自定义返回内容,那么你就重写readResolve方法,我会去找你有没有实现这个方法,如果有实现那么就用你的,如果没有实现,那么就按照反序列化后的对象作为返回参数。
通过这个源码分析我们可以得知,增加readResolve()方法返回实例解决了单例模式被破坏的问题,但是实际上是被实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的频率加快,那么意味内存分配开销也越大。所以这种方法不能从根本上解决问题。
因此,我们引入一种注册式单例模式。
注册式单例模式
注册式单例又称登记式单例,就是将每一个实例注册到一个地方,使用唯一标识来获取对象,大家以后都从这里取就完事了。注册式单例又分为两种:一种是枚举式单例,一种是容器式单例。
枚举式单例模式
直接上代码
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
尝试反序列化破坏枚举式单例
public class EnumTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
EnumSingleton instance = EnumSingleton.getInstance();
instance.setData(new Object());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
oos.writeObject(instance);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));
EnumSingleton instance2 = (EnumSingleton) ois.readObject();
System.out.println(instance.getData());
System.out.println(instance2.getData());
}
}
运行结果:
由运行结果可知,即使通过反序列化,生成的仍是同一个对象。
同样的,我们可以通过readOjbect0方法去追溯,而枚举类是调用readEnum方法,方法如下:
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
通过Enum<?> en = Enum.valueOf((Class)cl, name);
可以看出,枚举类型其实通过类名和类对象找到一个唯一的枚举对象,因此枚举对象不可能被类加载器加载多次。
这时候想到一个问题,如果使用反射能否欧怀枚举式单例模式呢?
尝试反射破坏枚举式单例
public class ReflectEnum {
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
EnumSingleton singleton = (EnumSingleton) c.newInstance();
System.out.println(singleton);
}catch (Exception e){
e.printStackTrace();
}
}
}
可以看到我们在使用构造方法的时候它先判断你这个构造方法是否是构建枚举类的,如果是的话就直接抛出异常。
可能在构造方法那里,有小伙伴对它的构造参数不理解,这个涉及到jvm的语法糖,不过多解释,但是我们可以通过反编译来得到,如下,我们通过jad反编译.class文件可以得到:
jad -sjava EnumSingleton.java
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingleton.java
package com.test.singleton;
public final class EnumSingleton extends Enum
{
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(com/test/singleton/EnumSingleton, name);
}
private EnumSingleton(String s, int i)
{
super(s, i);
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
this.data = data;
}
public static EnumSingleton getInstance()
{
return INSTANCE;
}
public static final EnumSingleton INSTANCE;
private Object data;
private static final EnumSingleton $VALUES[];
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
可以看到其实底层是通过继承Enum函数的,这也符合我们的“java一切皆对象”的思想。这里的枚举类构造方法的参数是String和int类型,然后再调用父类的构造方法,到这就解密了反射下无法生成新的枚举对象,对于注册时枚举类来说是安全的。
到这里我们其实可以印证jvm在反射和反序列化其实都有考虑到枚举类单例形式的安全,也就是说我们如果使用注册式枚举类相对其他单例模式,也是更安全的。
容器式单例模式
我们知道Spring框架中默认的作用于是单例的,而对于Spring来说是怎么保证单例的呢,其实在Spring中是通过一个键值对的形式来保存的,为了保证线程安全是使用了ConcurrentHashMap,当需要获取单例对象的时候,都可以去访问这个容器,在框架的加载阶段,会对相应的包进行扫描,并通过构造器初始化bean,再以beanName—bean这样的键值对的关系存放到容器中。
所以在本篇文章中,其实是更推荐使用枚举市的单例,当然,其他的单例模式我们也是可以用的,前提是保证能够安全的情况下,对于动态形式的单例我们可以通过容器来存放,当然,开发人员都要遵循对这个容器的共识,而不能每个人都独写一份。
单例模式小结
单例模式可以保证内存里只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。
篇幅稍长,感谢您的阅读,作者能力有限,如有勘误,敬请指导,不胜感激
更多推荐
所有评论(0)