设计模式之单例模式

    xiaoxiao2021-03-25  71

    1 单例模式简介

    1.1 介绍

    单例模式应该是日常使用最为广泛的一种模式了。他的作用是确保某个类只有一个实例,避免产生多个对象消耗过多的资源。比如对数据库的操作时;在一个应用中只有一个ImageLoader实例,这个ImageLoader又含有线程池、缓存系统、网络请求等,很消耗资源,就可以使用单例模式。

    1.2 单例模式定义

    确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

    1.3 使用场景

    避免某个类产生多个对象而消耗过多的资源,确保某个类在程序中只有一个实例。比如我们使用的图片加载器ImageLoader。往往单例创建的对象,耗费的资源都比较多,所以在初始化单例对象的时候就显得尤为重要了,接下来,我们就来聊一聊单例的几种实现方式。

    1.4 使用单例模式需要注意的关键点

    (1)将构造函数访问修饰符设置为private (2)通过一个静态方法或者枚举返回单例类对象 (3)确保单例类的对象有且只有一个,特别是在多线程环境下 (4)确保单例类对象在反序列化时不会重新构建对象

    2 单例模式UML类图

    3 单例模式的六种写法

    3.1 饿汉式

    /** * 饿汉式实现单例模式 */ public class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; } }

    饿汉式顾名思义,就是这个汉子很饿,一上来就把单例对象创建出来了,要用的时候直接返回即可,这种是单例模式中最简单的一种实现方式。但是问题也比较明显。单例在还没有使用到的时候,初始化就已经完成了。如果程序从头到位都没用使用这个单例的话,单例的对象还是会创建,这就造成了不必要的资源浪费。

    3.2 懒汉式

    /** * 懒汉式实现单例模式 */ public class Singleton { private static Singleton instance; private Singleton() { } // synchronized方法,多线程情况下保证单例对象唯一 public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

    (1)饿汉式也顾名思义,就是这个汉子比较懒,一开始的时候什么也不做,知道要使用的时候采取创建实例的对象。getInstance()方法中添加了synchronized关键字,使其变成一个同步方法,目的是为了在多线程环境下保证单例对象唯一。 (2)优点: 只有在使用时才会实例化单例,一定程度上节约了资源。 (3)缺点: 第一次加载时要立即实例化,反应稍慢。每次调用getInstance()方法都会进行同步,这样会消耗不必要的资源。这种模式一般不建议使用。

    3.3 DCL(Double CheckLock)实现单例(常用)

    3.3.1 DCL实现单例模式

    ** * DCL实现单例模式 */ public class Singleton { private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { // 两层判空,第一层是为了避免不必要的同步 // 第二层是为了在null的情况下创建实例 if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

    (1)优点: 资源利用率高,既能够在需要的时候才初始化实例,又能保证线程安全,同时调用getInstance()方法不进行同步锁,效率高。 (2)缺点: 第一次加载时稍慢,由于Java内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷,虽然发生概率很小。 因此添加一个volatile关键字,因为在这里会有DCL失效问题,原因是编译器为了提高执行效率的指令重排。只要认为在单线程下是没问题的,它就可以进行乱序写入!以保证不要让cpu指令流水线中断。

    3.3.2 JVM的底层机制和执行流程

    3.3.3 原因分析

    在3.3.1 DCL实现单例模式中的instance = new Singleton();到底发生了什么?

    memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance = memory; //3:设置instance指向刚分配的内存地址

    上面的伪代码中2、3步重排序之后的执行时序如下:

    memory = allocate(); //1:分配对象的内存空间 instance = memory; //3:设置instance指向刚分配的内存地址 //注意,此时对象还没有被初始化! ctorInstance(memory); //2:初始化对象

    为了更好的理解intra-thread semantics(线程内语义),请看下面的示意图(假设一个线程A在构造对象后,立即访问这个对象)。只要保证2排在4的前面,即使2和3之间重排序了,也不会违反intra-thread semantics。 下面,再让我们看看多线程并发执行的时候的情况。如果发生重排序,另一个并发执行的线程B就有可能在第4行判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!下面是这个场景的具体执行时序: 最后,线程有可能得到一个不为null,但是构造不完全的instance对象(没经过:2.初始化对象步骤)。

    3.3.4 解决方法

    (1)不允许2和3重排序。在JDK1.5之后,具体化了volatile关键字,只要定义时加上他,可以保证执行的顺序,虽然会影响性能。这种方式第一次加载时会稍慢,在高并发环境会有缺陷,但是一般能够满足需求。 (2)方官方比较推荐的一种方案(effective java 2nd),请看下一节:3.4 静态内部类(推荐)。

    3.3.5 参考链接

    双重检查锁定与延迟初始化

    3.4 静态内部类(推荐)

    /** * 静态内部类实现单例模式 */ public class Singleton { private Singleton() { } public static Singleton getInstance() { return SingletonHolder.instance; } /** * 静态内部类 */ private static class SingletonHolder { private static Singleton instance = new Singleton(); } }

    因为内部静态类是要在有引用之后才会转载到内存的,所以第一次加载Singleton类时不会初始化instance,只有在第一次调用getInstance()方法时,虚拟机会加载SingletonHolder类初始化instance,保证了单例对象的唯一。 因为getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本,实现了懒加载。因为静态的域只会在虚拟机转载类似初始化一次,并有虚拟机保证线程安全。

    3.5 枚举单例

    /** * 枚举实现单例模式 */ public enum SingletonEnum { INSTANCE; public void doSomething() { System.out.println("do something"); } }

    默认枚举实例的创建是线程安全的,即使反序列化也不会生成新的实例,任何情况下都是一个单例。优点: 简单!

    3.6 容器实现单例

    /** * 容器类实现单例模式 */ public class SingletonManager { private static Map<String, Object> objMap = new HashMap<String, Object>(); public static void regsiterService(String key, Object instance) { if (!objMap.containsKey(key)) { objMap.put(key, instance); } } public static Object getService(String key) { return objMap.get(key); } }

    SingletonManager可以管理多个单例类型,使用时根据key获取对象对应类型的对象。这种方式可以通过统一的接口获取操作,隐藏了具体实现,降低了耦合度。

    4 防止单例模式被JAVA反射攻击

    (1)举例,通过JAVA的反射机制来“攻击”单例模式

    public class ElvisReflectAttack{ public static void main(String[] args) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { Class<?> classType = Singleton.class; Constructor<?> c = classType.getDeclaredConstructor(null); c.setAccessible(true); Elvis e1 = (Singleton)c.newInstance(); Elvis e2 = Singleton.getInstance(); System.out.println(e1==e2); } } // 运行结果:false

    小结:通过反射获取构造函数,然后调用setAccessible(true)就可以调用私有的构造函数,所有e1和e2是两个不同的对象。如果要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。 (2)以DCL实现单例模式为例

    /** * DCL实现单例模式 */ public class Singleton { private volatile static Singleton instance = null; private static boolean flag = false; private Singleton() { synchronized(Singleton.class) { Log.e("Interview_log", "flag:" + flag); if(!flag) { flag = true; } else { //在被要求创建第二个实例的时候抛出异常,无法生成对象实例 throw new RuntimeException("单例模式被侵犯!"); } } } public static Singleton getInstance() { // 两层判空,第一层是为了避免不必要的同步 // 第二层是为了在null的情况下创建实例 if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

    (3)测试demo

    private void reflectAttack() { Class<?> classType = Singleton.class; Singleton singleton1 = Singleton.getInstance(); Constructor<?> c = null; try { c = classType.getDeclaredConstructor(); c.setAccessible(true); Singleton singleton2 = null; try { singleton2 = (Singleton) c.newInstance();//反射得到的对象 } catch (Exception e) { Log.e("Interview_log", "Exception:" + e.toString()); e.printStackTrace(); } Log.e("Interview_log", "反射攻击2:" + singleton1.toString()); if (singleton2 == null) { Log.e("Interview_log", "反射攻击3:反射得到的对象为null"); } else { Log.e("Interview_log", "反射攻击3:" + singleton2.toString()); } Log.e("Interview_log", "反射攻击4:" + (singleton1 == singleton2)); } catch (NoSuchMethodException e) { e.printStackTrace(); } }

    (3)结果 (4)学习链接 如何防止单例模式被JAVA反射攻击

    (5)经过验证,枚举单例可以防止单例模式被“攻击”,也可以防止序列化破坏单例模式。

    5 Android源码中的单例模式

    在Android系统中,我们经常会通过Context获取系统级别的服务,如WindowsManagerService、ActivityManagerService等,更常用的是一个LayoutInflater的类,这些服务会在合适的时候以单例的形式注册在系统中,在我们需要的时候就通过Context的getSystemService(String name)获取。

    6 运用单例模式

    7 总结

    7.1 优点

    (1)由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁的创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。 (2)单例模式可以避免对资源的多重占用,例如一个文件操作,由于只有一个实例存在内存中,避免对同一资源文件的同时操作。 (3)单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。

    7.2 缺点

    (1)单例模式一般没有接口,扩展很困难,若要扩展,只能修改代码来实现。 (2)单例对象如果持有Context,那么很容易引发内存泄露。此时需要注意传递给单例对象的Context最好是Application Context。

    8 参考文章与链接

    《Android源码设计模式解析与实战》

    《设计模式之禅》

    单例模式的6种实现方式

    单例模式的五种实现方式

    DCL失效问题的探讨

    转载请注明原文地址: https://ju.6miu.com/read-35717.html

    最新回复(0)