读懂设计模式之单例模式
本文参考了以下博客
https://www.cnblogs.com/xiaobai1226/p/8487696.html
https://blog.csdn.net/qq_35860138/article/details/86477538
https://blog.csdn.net/li295214001/article/details/48135939/
单例模式是一种对象创建型模式,使用单例模式,可以保证为一个类只生成唯一的一个实例对象。也就是说,在整个程序空间中,该类只存在一个实例对象。
其实,GoF对单例模式的定义是:保证一个类,只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。
那么,我们为什么要用单例模式呢?
这是因为在应用系统开发时,我们常常有以下需求:
-
1、在多个线程之间,比如servlet环境,共享同一个资源或者操作同一个对象。
-
2、在整个程序空间使用全局变量,共享资源。
-
3、在大规模系统中,为了性能的考虑,需要节省对象的创建时间等等。
因为单例模式可以保证为一个类只生成唯一的实例对象,所以这些情况,单例模式就派上用场了。
单例符合以下基本特点
- 声明静态私有类变量,且立即实例化,保证实例化一次
- 私有构造,防止外部实例化(通过反射是可以实例化的,不考虑此种情况)
- 提供public的
getInstance()
方法供外部获取单例实例
1、懒汉式
特点:懒加载,在实例被调用时才初始化。但是线程不安全。
public class LazyInstance {
private static LazyInstance instance = null;
/**
* 构造函数私有化
*/
private LazyInstance() {
}
/**
* 提供一个全局的静态方法
*/
public static LazyInstance getInstance() {
if (instance == null) {
instance = new LazyInstance();
}
return instance;
}
}
2、加锁懒汉式
由于线程不安全,我们可以通过synchronized
关键字进行处理。此时代码如下
public class LazySynchronized {
private static LazySynchronized instance = null;
/**
* 构造函数私有化
*/
private LazySynchronized() {
}
public static synchronized LazySynchronized getInstance() {
if (instance == null) {
instance = new LazySynchronized();
}
return instance;
}
}
这种实现方式虽然线程安全,但是每次获取实例都要加锁,耗费资源,其实只要实例已经生成,以后获取就不需要再锁了。
基于这些缺点,我们可以在使用双重检查,对懒汉式进行进一步升级
3、双重检查
public class LazyDoubleCheck implements Serializable {
private static LazyDoubleCheck instance = null;
/**
* 构造函数私有化
*/
private LazyDoubleCheck() {
}
public static LazyDoubleCheck getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheck.class) {
if (instance == null) {
instance = new LazyDoubleCheck();
}
}
}
return instance;
}
}
这样写,只把新建实例的代码放到同步锁中,为了保证线程安全再在同步锁中加一个判断,
虽然看起来更繁琐,但是同步中的内容只会执行一次,执行过后,以后经过外层的if判断后,都不会在执行了,
所以不会再有阻塞。程序运行的效率也会更加的高。
这种方式看似很好的解决了同步的问题,但是其中还是有个坑。当多个线程访问这个方法时,
可能会返回还未完成初始化的对象!
问题就在于instance = new LazyDoubleCheck();
根据Java类的初始化过程,步骤instance = new LazyDoubleCheck();
并不是原子性的。
其中大概可以分为三个步骤
- 分配内存给对象
- 初始化对象
- 设置instance指向刚分配的内存地址
但是在实际执行中代码可能会被重排序,如下所示
- 分配内存给对象
- 设置instance指向刚分配的内存地址
- 初始化对象
在Java语言规范中,所有线程在执行Java程序时,必须要遵守intra-thread semantics规定。
intra-thread semantics保证重排序不会改变单线程内的程序执行结果。会允许那些在单线程内,不会改变单线程程序执行结果的重排序。例如上面的2和3步骤虽然被重排序了,但并不会影响程序执行结果。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。
为了更好的理解这个问题,参考一下图示
线程1执行到instance = new LazyDoubleCheck()
时发生重排序先执行第三步,此时instance被赋值但是还未初始化对象,这时线程2访问到第一个if中,由于此时判断instance不为null就直接返回此对象,线程2获得的就是还未初始化完全的对象。注意:由于重排序问题并不一定会发生
针对上面出现的问题,我们可以有两个解决方案
1、不允许步骤2和3重排序
2、允许重排序,但是不允许其他线程"看到"重排序过程
针对方案一有以下实现,基于volatile
关键字禁止重排序
4、基于volatile
的双重检查
public class LazyDoubleCheck implements Serializable {
private static volatile LazyDoubleCheck instance = null;
/**
* 构造函数私有化
*/
private LazyDoubleCheck() {
}
public static LazyDoubleCheck getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheck.class) {
if (instance == null) {
instance = new LazyDoubleCheck();
}
}
}
return instance;
}
}
volatile
关键字具有内存可见性,关于这个以后会写博客细谈
针对方案二,我们通过静态内部类的方式来实现。
5、静态内部类
public class StaticInnerClass {
private static class InstanceHolder {
private static final StaticInnerClass instance = new StaticInnerClass();
}
/**
* 构造函数私有化
*/
private StaticInnerClass() {
}
public static StaticInnerClass getInstance() {
return InstanceHolder.instance;
}
}
这种实现方式是基于类的初始化锁来实现类的懒加载安全性
利用了ClassLoader
的机制来保证初始化instance时只有一个线程,同时实现了延时加载
优点:既避免了同步带来的性能损耗,又能够延迟加载
关于双重检查锁的原理还可以参考这篇博客https://blog.csdn.net/li295214001/article/details/48135939/
6、饿汉式
特点:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快,同时无法做到延时加载
public class Hungry {
private static final Hungry hungry = new Hungry();
/**
* 构造函数私有化
*/
private Hungry() {
}
public static Hungry getInstance() {
return hungry;
}
}
好处:线程安全;获取实例速度快 缺点:类加载即初始化实例,内存浪费。如果实例未使用,仍然会生成占用内存
以上方式都各自实现了单例模式,但是并不能完全保障单例安全。当我们对一个对象进行序列化与反序列化后,上述单实例就无法保证
public static void main(String[] args) throws Exception {
Hungry s = Hungry.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
oos.writeObject(s);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Hungry s1 = (Hungry) ois.readObject();
ois.close();
System.out.println(s + "\n" + s1);
}
当我们执行上述序列化代码时,程序中就会生成两个单例对象,针对这种方式我们要如何保证单例呢
7、序列化安全的单例
此处我们对饿汉式单例进行改造,使其保证序列化时仍然是单例。
解决方案:在单例中增加readResolve
方法
public class Hungry implements Serializable{
private static final Hungry hungry = new Hungry();
/**
* 构造函数私有化
*/
private Hungry() {
}
public static Hungry getInstance() {
return hungry;
}
public Object readResolve(){
return hungry;
}
}
此时如果我们再测试序列化,就会发现返回的对象是同一个。这是为什么呢?为什么重写readResolve()
方法就能实现序列化安全呢。关键还是在于ObjectInputStream
的readObject()
方法。查看源码,我们就能一探究竟
以下JDK源码基于JDK1.8.0_162
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
int outerHandle = passHandle;
try {
//返回的obj是readObject0()生成的
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
//中间省略部分代码
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
查看方法可以看到返回的obj是readObject0()生成的,再进入readObject0()中
private Object readObject0(boolean unshared) throws IOException {
//省略部分代码
try {
switch (tc) {
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
//可以看出object类型返回的对象都是以下返回的
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
//以下部分代码省略
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
从以上部分代码可以看出返回的代码是checkResolve(readOrdinaryObject(unshared))
,首先查看readOrdinaryObject(unshared)
方法
private Object readOrdinaryObject(boolean unshared) throws IOException{
//省略部分代码。。。
//这个obj就是返回的对象,只需要关注这个对象就行了
Object obj;
try {
//如果实现了serializable/externalizable接口isInstantiable()就会返回true
//此时基于反射生成了实例
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//中间省略部分代码...
//进入if条件中,
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) {
//这里通过反射会调用方法生成rep对象,在下面rep会赋值给obj对象
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);
}
}
//此时obj指向刚才通过invokeReadResolve()方法生成的对象
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
在readOrdinaryObject
方法中待返回的对象obj
首先指向通过反射生成的实例
obj = desc.isInstantiable() ? desc.newInstance() : null;
(因为实现了serializable/externalizable接口isInstantiable()
返回true)。
在下面的if条件中生成了rep对象,并且在下面将rep赋值给obj
handles.setObject(passHandle, obj = rep);
因此将重点放在Object rep = desc.invokeReadResolve(obj);
中,查看invokeReadResolve
方法
Object invokeReadResolve(Object obj)throws IOException, UnsupportedOperationException {
requireInitialized();
if (readResolveMethod != null) {
try {
//就是一个反射调用,关键是这个readResolveMethod是什么
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
//省略部分代码。。。
}
} else {
throw new UnsupportedOperationException();
}
}
在类中查找readResolveMethod
可以看到就是ObjectStreamClass
中定义的一个私有变量
private Method readResolveMethod;
继续查找可以看到赋值readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
就是定义readResolve
的Method对象,通过反射调用readResolve
方法,将产生的对象在返回给obj
再返回。
此时返回到readObject0
方法中,readOrdinaryObject(unshared)
返回的值传到checkResolve()
中
private Object checkResolve(Object obj) throws IOException {
if (!enableResolve || handles.lookupException(passHandle) != null) {
return obj;
}
//又调用了resolveObject方法,将返回的对象返回去
Object rep = resolveObject(obj);
if (rep != obj) {
// The type of the original object has been filtered but resolveObject
// may have replaced it; filter the replacement's type
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, rep);
}
return rep;
}
继续查看resolveObject
方法定义
protected Object resolveObject(Object obj) throws IOException {
return obj;
}
在resolveObject
中直接将obj返回了。此时所有流程基本走完。
到这里我们就可以知道为什么在类中定义readResolve
方法返回单例对象就可以防止序列化破坏单例了。
但是注意在上面代码中有一步 obj = desc.isInstantiable() ? desc.newInstance() : null;
虽然最后返回的是单例对象,但其实在执行过程还是通过反射生成了新的对象,虽然最后返回的并不是这个对象。
那么如何防止反射生成多个对象呢?
8、防止反射的单例
对于饿汉模式来说,运行以下代码通过反射创建对象仍然会产生多个实例
public static void main(String[] args) throws Exception {
Class objClass = Hungry.class;
Constructor constructor = objClass.getDeclaredConstructor();
constructor.setAccessible(true);
Hungry instance = Hungry.getInstance();
Hungry newInstance = constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(newInstance == instance);
}
执行结果
Hungry@6d6f6e28
Hungry@135fbaa4
false
此时就需要对私有构造器改造一下,禁止其反射调用
private Hungry() {
if (hungry != null) {
throw new RuntimeException("单例构造器禁止反射!");
}
}
此时在执行上述代码就会报错
Exception in thread "main" java.lang.reflect.InvocationTargetException
at ReflectTest.main(ReflectTest.java:24)
Caused by: java.lang.RuntimeException: 单例构造器禁止反射!
at Hungry.(Hungry.java:25)
... 5 more
对于静态内部类实现的单例模式也是这种方式防止反射
private StaticInnerClass() {
if (InstanceHolder.instance != null) {
throw new RuntimeException("单例构造器禁止反射!");
}
}
注意,对于这种调用getInstance
方法时就已经完成类初始化的单例(饿汉模式和静态内部类),这种方式可以防止反射。但是对于类似懒汉模式这种,调用getInstance
才会初始化的单例,可能会有一些问题。
注意在上面反射创建对象的代码中,是先调用的getInstance
方法,然后才使用反射创建了对象,这样是没有问题的。但如果两者调换的位置,先通过反射创建对象,然后再调用getInstance
方法。即便我们再构造器中添加代码仍然会出现问题
private LazyInstance() {
if (instance != null) {
throw new RuntimeException("单例构造器禁止反射!");
}
}
public static void main(String[] args) throws Exception {
Class objClass = LazyInstance.class;
Constructor constructor = objClass.getDeclaredConstructor();
constructor.setAccessible(true);
LazyInstance newInstance = constructor.newInstance();
LazyInstance instance = LazyInstance.getInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(newInstance == instance);
}
运行结果
LazyInstance@6d6f6e28
LazyInstance@135fbaa4
false
当使用反射创建对象时,由于还没调用getInstance
方法,此时instance还为null,就不会抛出异常。
当然我们可以设置一个变量flag标记是否初始化,然后再构造器中通过该变量进行判断,但是既然构造器可以通过反射调用,设置变量仍然是可以被反射修改的
有没有完美的单例模式吗
9. 枚举类型单例
这是单例模式的完美实现
public enum EnumSingleton {
/**
* 单实例
*/
INSTANCE
public void doSomething() {
System.out.println("you can do something");
}
public static void main(String[] args) {
EnumSingleton.INSTANCE.doSomething();
}
}
线程安全,序列化安全,自带防止反射
理论上原型模式也会破坏单例,但是基本上没人会在单例上运用原型模式
单例在JDK中的使用
java.lang.Runtime
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class Runtime
are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the Runtime
object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
}