读懂设计模式之单例模式

本文参考了以下博客

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对单例模式的定义是:保证一个类,只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。

单例具有以下基本特点

  • 声明静态私有类变量,且立即实例化,保证实例化一次
  • 私有构造,防止外部实例化
  • 提供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();并不是原子性的。
其中大概可以分为三个步骤

    1. 分配内存给对象
    1. 初始化对象
    1. 设置instance指向刚分配的内存地址

但是在实际执行中代码可能会被重排序,如下所示

    1. 分配内存给对象
    1. 设置instance指向刚分配的内存地址
    1. 初始化对象

在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()方法就能实现序列化安全呢。关键还是在于ObjectInputStreamreadObject()方法。查看源码,我们就能一探究竟

以下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<Hungry> objClass = Hungry.class;
        Constructor<Hungry> 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.<init>(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<LazyInstance> objClass = LazyInstance.class;
        Constructor<LazyInstance> 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();
    }

}

这种方式实现了线程安全,序列化安全,自带防止反射

后记:理论上原型模式也会破坏单例,但是基本上没人会在单例上运用原型模式,在下一篇原型模式之后再补充

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值