设计模式 (一) 经过理论 + 代码示例 + Android 源码中单例模式来学习单例

介绍

单例模式是应用最为普遍的模式之一,也多是不少入门或初级工程师惟一会使用的设计模式之吧,在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只须要拥有一个实例类。有利于咱们的调用,避免一个相同的类重复建立实例,好比一个网络请求,图片请求/下载,数据库操做等,若是频繁建立同一个相同对象的话,很消耗资源,所以,没有理由让它们构造多个实例。全局都须要使用这个功能的时候,避免重复建立,就能够用单例,这就是单例使用场景。java

定义

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

使用场景

应用中重复使用某个类时,为了不屡次建立产生的资源消耗,那么这个时候就能够考虑使用单例设计模式。git

单例 UML 类图

czjKW.png

实现单例模式主要有以下几个关键点:github

  1. 构造函数不对外开放,通常为 private;
  2. 经过一个静态方法或者枚举返回单例对象;
  3. 确保单例类的对象有且只有一个,尤为是在多线程环境下;
  4. 确保单例类对象在反序列化时不会被从新构建对象。

单例示例

饿汉式

单例模式是设计模式中比较简单的,只有一个单例类,没有其余层次结构与抽象。该模式须要确保该类只能生成一个对象,一般是该类须要消耗较多的资源或者没有多个实例的状况。例以下面的代码:数据库

public class DaoManager {

    /** * 饿汉式单例 */
    private static DaoManager instance = new DaoManager();

    private DaoManager(){}

    public static DaoManager getInstance(){
        return instance;
    }
}
复制代码

测试编程

@Test
    public void test(){
        String dao = DaoManager.getInstance().toString();
        String dao1 = DaoManager.getInstance().toString();
        String dao2 = DaoManager.getInstance().toString();
        String dao3 = DaoManager.getInstance().toString();


        System.out.println(dao);
        System.out.println(dao1);
        System.out.println(dao2);
        System.out.println(dao3);
    }
复制代码

Output设计模式

com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
复制代码

从上面代码能够看到 DaoManager 不能经过 new 的形式构造对象,只能经过 getInstance() 拿到实例,而 DaoManager 对象是静态的,那么在声明的时候已经初始化了,这就保证了对象的惟一性,从输入结果中发现, DaoManager 四次输出的地址都是同样的。这个实现的核心在与将 DaoManager 类的构造方法私有化,使得外部程序不能经过构造来 new 对象,只能经过 getInstance() 来返回一个对象。安全

懒汉模式

懒汉模式是声明了一个静态对象,而且在第一调用的时候进行初始化,而上面的饿汉纸则是在声明的时候已经初始化了。懒汉式的实现以下:网络

public class DaoManager2 {
    
    private static DaoManager2 instance;
    
    private DaoManager2(){}

    /** * 保证线程安全的懒汉式 * @return */
    public static synchronized DaoManager2 getInstance(){
        if (null == instance) {
            instance = new DaoManager2();
        }
        return instance;
    }
}
复制代码

细心的读者可能已经发现了,getInstance() 方法中添加了 synchronized 关键字, getInstance 是一个同步方法,保证了在多线程状况下单例对象惟一性。细想下,你们可能会发现一个问题,即便 instance 已经被初始化,每次调用都会进行同步检查,这样会消耗没必要要的资源,这也是懒汉单例模式存在的最大问题。多线程

最后总结一下,懒汉单例模式的优势是单例只有再使用的时候进行初始化,在必定程度上节约了资源;缺点是第一次加载时须要进行初始化,反应稍慢,最大的问题就是每次调用的时候 getInstance 都进行同步,形成没必要要的开销。这种模式通常不建议使用。

Double Check Lock 实现单例

DCL 方式实现单例模式的有点是既可以在须要时初始化单例,又能保证线程安全,且单例对象初始化后调用 instance 不进行同步锁,代码以下:

public class DaoManager3 {

    private static DaoManager3 sinstance;

    private DaoManager3() {
    }

    /** * 保证线程安全的懒汉式 * * @return */
    public static DaoManager3 getInstance() {
        if (null == sinstance) {
            synchronized (DaoManager3.class) {
                if (null == instance)
                    sinstance = new DaoManager3();
            }
        }
        return sinstance;
    }
}

复制代码

本段代码的亮点就在于 getInstance 方法上,能够看到 getInstance 方法对 instance 进行了两次判空;第一层判断主要是为了不没必要要的同步,第二层的判断则是为了在 null 的状况下建立实例。是否是看起来有点迷糊,下面在来解释下:

sinstance = new DaoManager3();
复制代码

这个步骤,其实在jvm里面的执行分为三步:

  1. 在堆内存开辟内存空间;
  2. 在堆内存中实例化 DaoManager3 里面的各个参数;
  3. 把对象指向堆内存空间;

因为在 JDK 1.5 之前 Java 编译器容许处理器乱序执行,以及 JMM 没法保证 Cache, 寄存器(Java 内存模型)保证按照 1,2,3 的顺序执行。因此可能在 2 还没执行时就先执行了 3,若是此时再被切换到线程 B 上,因为执行了 3,sinstance 已经非空了,会被直接拿出来用,这样的话,就会出现异常。并且不易复现不易跟踪是一个隐藏的 BUG。

不过在 JDK 1.5 以后,官方也发现了这个问题,故而具体化了 volatile ,即在 JDK 1.6 之后,只要定义为 private volatile static DaoManager3 sinstance ; 就可解决 DCL 失效问题。volatile 确保 sinstance 每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。

DCL 优势:资源利用率高,第一次执行 getInstance 时单例对象才会被实例化,效率高。

DCL 缺点:第一次加载时,反应稍慢,也因为 Java 内存模型的缘由偶尔会失败。在高并发环境下也有必定的缺陷,虽然发生几率很小。

DCL 模式是使用最多的模式,它可以在须要时才被实例化,而且可以在绝大多数场景下保证单例对象的惟一性,除非你的代码在并发场景比较复杂或者低于 JDK 6 版本下使用,不然,这种方式通常可以知足需求。

静态内部类单例模式

DCL 虽然在必定程度上解决了资源消耗、多余的同步、线程安全等问题,可是,它仍是在某些状况下出现失效的问题,这个问题被称为双重检查锁定失效,在《Java 并发编程实践》一书的最后谈到了这个问题,并指出这种 “优化” 是丑陋的,不同意使用。而建议使用以下的代码替代。

public class DaoManager4 {
    
    private DaoManager4(){}

    public static DaoManager4 getInstance(){
        return DaoManager4Holder.sInstance;
    }

    /** * 静态内部类 * */
    private static class DaoManager4Holder{
        private static final DaoManager4 sInstance = new DaoManager4();
    }
}
复制代码

那么,静态内部类又是如何实现线程安全的呢?首先,咱们先了解下类的加载时机。

类加载时机:JAVA 虚拟机在有且仅有的 5 种场景下会对类进行初始化。

  1. 遇到 new、getstatic、setstatic 或者 invokestatic 这4个字节码指令时,对应的 java 代码场景为:new 一个关键字或者一个实例化对象时、读取或设置一个静态字段时 ( final 修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,若是类没进行初始化,须要先调用其初始化方法进行初始化。
  3. 当初始化一个类时,若是其父类还未进行初始化,会先触发其父类的初始化。
  4. 当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
  5. 当使用 JDK 1.7 等动态语言支持时,若是一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,而且这个方法句柄所对应的类没有进行过初始化,则须要先触发其初始化。 这 5 种状况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此以外的全部引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

咱们再回头看下 getInstance() 方法,调用的是 DaoManager4Holder.sInstance ,取的是DaoManager4Holder 里的 sInstance 对象,跟上面那个 DCL 方法不一样的是 ,getInstance()方法并无屡次去 new 对象,故无论多少个线程去调用 getInstance() 方法,取的都是同一个sInstance 对象,而不用去从新建立。当 getInstance() 方法被调用时,DaoManager4Holder 才在 DaoManager4 的运行时常量池里,把符号引用替换为直接引用,这时静态对象sInstance 也真正被建立,而后再被 getInstance() 方法返回出去,这点同饿汉模式。那么sInstance 在建立过程当中又是如何保证线程安全的呢?在《深刻理解JAVA虚拟机》中,有这么一句话:

虚拟机会保证一个类的 () 方法在多线程环境中被正确地加锁、同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 () 方法,其余线程都须要阻塞等待,直到活动线程执行 () 方法完毕。若是在一个类的 () 方法中有耗时很长的操做,就可能形成多个进程阻塞 (须要注意的是,其余线程虽然会被阻塞,但若是执行 () 方法后,其余线程唤醒以后不会再次进入 () 方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞每每是很隐蔽的。

故而,能够看出 sInstance 在建立过程当中是线程安全的,因此说静态内部类形式的单例可保证线程安全,也能保证单例的惟一性,同时也延迟了单例的实例化。

那么,是否是能够说静态内部类单例就是最完美的单例模式了呢?其实否则,静态内部类也有着一个致命的缺点,就是传参的问题,因为是静态内部类的形式去建立单例的,故外部没法传递参数进去,例如 Context 这种参数,因此,咱们建立单例时,能够在静态内部类与 DCL 模式里本身斟酌。

枚举单例

前面讲解了几个单例模式的实现方式,这几个实现方式不是稍显麻烦就是会在某种状况下出现问题,那么还有没有更简单的实现方式勒? 咱们先来看看下面的实现方式。

public enum  DaoManager5 {
    
    INSTANCE;
    
    public void doSomething(){
        Log.i("DAO->","枚举单例");
    }
}
复制代码

没错,就是枚举单例!

写法简单简单是枚举单例最大的优势,枚举在 Java 中与普通的类时同样的,不只可以拥有字段,还可以拥有本身的方法。最重要的是默认枚举实例的建立是线程安全的,而且在任何状况下它都是一个单例。

优势:枚举自己是线程安全的,且能防止经过反射和反序列化建立实例。

缺点:对 JDK 版本有限制要求,非懒加载。

使用容器实现单例模式

学习了上面 5 大单例模式,最后在来介绍一种容器单例模式,请看下面代码实现:

public class DaoManager6 {

    /** * 定义一个容器 */
    private static Map<String,Object> singletonMap = new HashMap<>();
    
    private DaoManager6(){}
    
    public static void initDao(String key,Object instance){
        if (!singletonMap.containsKey(key)){
            singletonMap.put(key,instance);
        }
    }
    
    public static Object getDao(String key){
        return singletonMap.get(key);
    }
}
复制代码

在程序的初始,能够将单例类型注入到统一管理类中,在使用的时候根据 key 获取对应单例对象,而且在使用时能够经过统一的接口进行获取操做,下降了用户的使用成本,也对用户隐藏了具体实现,下降了耦合度。

Android 源码中单例模式

Android 源码中涉及了大量的单例模式,这里就拿较为熟悉的 context.getSystemService(String name); 容器单例模式,以 Context.LAYOUT_INFLATER_SERVICE 举例。

从 setContentView 入口,全方位分析 LayoutInflater

总结

单例模式在应用中时属于使用频率最高的一种设计模式了,可是因为客户端一般没有高并发的状况,所以,选择哪一种实现方式并不会有太大的影响。固然,考虑效率和并发的场景仍是推荐你们使用 DCL 或 静态内部类单例模式。

注意:若是单例对象必须持有参数的话,那么最好建议使用弱引用来接收参数,若是是 Context 级别的类型,建议使用 context.getApplication() 不然容易形成内存泄漏;

文章中出现的代码

感谢你的阅读,谢谢!

相关文章
相关标签/搜索