上期文章中,汇智妹跟大家分享了单例模式的概念、特点和常见的一些实现方式。
但是,其实当中也存在一些问题,尤其是在多线程高并发情况下,可以通过反射或者序列化的方式生成多个实例,这样一来也就破坏了单例。
首先看序列化破坏:
输出结果:
输出的singleTon和singleTon2的hashCode不一样,那么证明它们并不是同一个对象,证明singleTon类生成了多个不一样的实例对象,序列化破坏了单例设计模式。
怎么解决?
其实很简单,我们只需要在单例类中添加一个方法即可:
protected Object readResolve() throws ObjectStreamException {
return getInstance();
}
加了之后的结果:
分析:添加了readResolve() 这个自定义方法之后,ObjectInputStream 在读取对象的时候就会直接调用序列化类中的readResolve()方法返回实例,不会再创建新的实例,这样就防止了序列化破坏单例设计。
反射破坏:
public static void main(String[] args) {
SingleTon instanceOne = SingleTon.getInstance();
SingleTon instanceTwo = null;
try {
Constructor[] constructors =
SingleTon3.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
constructor.setAccessible(true);
instanceTwo = (SingleTon) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
我们发现两个的hashCode不一样,那么就意味着生成了多个实例对象,有没有防止反射破坏的实现方式呢?
枚举类:
public enum SingleTon{
INSTANCE;
}
public static void main(String[] args) {
SingleTon instanceOne = SingleTon.INSTANCE;
SingleTon instanceTwo = null;
try {
Constructor[] constructors =
SingleTon.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
constructor.setAccessible(true);
instanceTwo = (SingleTon) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
反射尝试破坏运行结果:
分析:JVM中枚举的实现默认就是线程安全的,并且枚举类还提供了拒绝JVM通过反射创建实例对象,这样就防止了反射破坏单例模式。
不足之处是枚举无法满足懒加载,并且枚举比静态实例变量要占用更多的内存。
上面提到了很多种高并发下线程安全的单例模式实现,能回答了这么多种方式并且可以对各种实现方式进行分析点评,其实已经深得面试官的喜爱了。
但是有的面试官又要问:“那么你项目开发中或者java流行框架spring中使用了那种方式的单例实现呢?”
Spring框架是java面试中老生常谈的问题,在spring中,bean的创建默认就是单例模式的设计。
那spring是饿汉式单例还是懒汉式单例呢?如果你不了解它,顺着面试官的提问回答是饿汉还是懒汉,那基本上这个问题上就玩完了。
Spring框架对单例的支持是采用注册(登记)模式进行实现的,我们直接来看spring登记模式的实现,新版的spring的注册模式的代码,从类AbstractBeanFactory的getBean(...) 方法中来看spring源码。
我们可以发现具体的单例实现在类DefaultSingletonBeanRegistry的
getSing leto()方法中:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null &&
this.isSingletonCurrentlyInCreation(beanName)) {
synchronized(this.singletonObjects)
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory =
(ObjectFactory)this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName,
singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
分析:spring的源码中可以看出,spring注册模式经过了一下几个步骤:
(1)先从注册表ConcurrentHashMap中获取单例对象,其中ConcurrentHashMap是一个线程安全的map集合;
(2)判断注册表中获取的实例对象是否为空,如果不为空则直接返回单例对象singletonObject;
(3)如果注册表中为空,则在同步锁中使用singletonFactory创建singletonObject并放入map集合中。
关于<单例模式>的介绍,我们从上下两篇文章中向大家介绍了所有最经典的写法以及背后的分析,不知各位程序员小哥哥们有没有认真学习,做到“一文在手,面试无忧”呢?
相信大家只要认真消化吸收,并且结合项目实践来熟练灵活运用,必定会在面试的过程中游刃有余,令面试官大声疾呼:神tm内行!