应用最广的模式--单例模式

本文出自博客Vander丶CSDN博客,如需转载请标明出处,尊重原创谢谢
博客地址:http://blog.csdn.net/l540675759/article/details/69488764


前言

1.本篇博客属于笔记性质,主要整理归纳知识点,方便日后.

2.本篇的内容是基于”Android源码设计模式”.

3.如果有不足的地方,请大家指出,作者会及时更改.

单例模式的介绍

单例模式是应用最广的模式之一,在应用这个模式时,单例对象的类必须保证只有一个实例存在.

许多时候整个系统中只需要拥有一个全局对象,这样有利于我们协调系统整体的行为.

如,在一个应用中,应该只有一个ImageLoader实例,这个ImageLoader中又包含线程池,缓存系统,网络请求等,很消耗资源,因此在这种情况下需要使用单例模式.

单例模式的定义

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

单例模式的使用场景

确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该只有一个.

例如:访问IO和数据库等资源.

单例模式UML类图

单例模式的类图
角色介绍:
Client —— 高层客户端(也可以理解成对单例类有需求的对象)
Singleton —— 单例类


单例模式的几种实现方式

懒汉式
public class Singleton {

    private static Singleton instance;

    //构造方法私有化
    private Singleton() {

    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉式特点:

1.懒汉单例模式的优点是单例只有在使用时才会被实例化,在一定程度上解决资源.

2.懒汉单例模式的缺点是第一次加载时需要及时进行实例化,反应稍慢,最大的问题是每次调用getInstance()都进行同步,造成不必要的开销.

这种模式一般情况下不建议使用.


Double CheckLock(DCL)实现单例
public class SingletonDCL {
    //在JDK1.5之后 volatile直接使Java处理器在创建这个对象上,顺序执行,不可乱序执行.
    private static volatile SingletonDCL instance;


    private SingletonDCL() {
    }

    public static SingletonDCL getInstance() {
        //第一道锁,主要是为了避免不必要的同步
        if (instance == null) {
            synchronized (SingletonDCL.class) {
                //第二道锁为了在null的情况下,创建实例.
                if (instance == null) {
                    instance = new SingletonDCL();
                }
            }
        }
        return instance;
    }
}

双重锁检查单例的特点:

1.资源利用率高,第一次执行getInstance时单例对象才会被实例化,实现了懒加载.

2.第一次加载时反应稍慢,并且在JDK1.5之前,不使用volatile关键字,可能在并发环境中出现问题.

DCL模式是使用最多的单例实现方式,它能够在需要时才实例化单例对象,并且能够在绝大多数场景下保证单例对象的唯一性.

除非你的代码在并发场景比较复杂或者低于JDK1.6版本下使用,否则,这种方式一般能够满足需求.


静态内部类单例模式
public class SingletonN {


    private SingletonN() {
    }

    /**
     * 静态内部类
     */
    private static class SingletonHolder {
        //内部类静态初始化
        private final static SingletonN sIntance = new SingletonN();
    }

    public static SingletonN getInstance() {
        return SingletonHolder.sIntance;
    }
}

DCL虽然在一定程序上解决了资源消耗、多余的同步、线程安全等问题,但是,它还是在某些情况下出现失效的问题。这个问题被称为双重检查锁定(DCL)失效,在《Java 并发编程实践》一书的最后谈到了这个问题,并指出这种优化还是存在问题的。

静态内部类单例模式的特点:

当第一次加载 Singleton 类时并不会初始化 sIntance,只有在第一次调用 Singleton 的 getInstance 方法时才会导致 sIntance 被初始化。

因为,第一次调用 getInstance 方法会导致虚拟机加载 SingletonHolder 类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式实现方法。


枚举单例
public enum SingletonEnum {

    SINGLETON_ENUM;

    public void doSomething(int i) {
        //默认枚举实例创建就是线程安全的
    }
}

枚举单例的特点:

首先,枚举单例的写法比较简单

枚举在 Java 中与普通的类是一样的,不仅能够有字段,还能够有自己的方法。最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。

而且枚举反序列化也不会重新生成新的实例。


使用容器实现单例模式
public class SingletonManager {

    //储存单例的容器
    private static Map<String, Object> objMap = new HashMap<>();

    private SingletonManager() {
    }

    /**
     * 添加单例容器
     *
     * @param key      单例容器的 key
     * @param instance 单例容器的 引用
     */
    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

    /**
     * 获取单例
     *
     * @param key 单例容器的 key
     * @return 容器中的单例对象
     */
    public static Object getService(String key) {
        return objMap.get(key);
    }
}

使用容器实现单例的特点:

首先需要在程序的初始,将多种单例类型注入到一个统一的管理类。

然后使用的时候根据 key 获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

个人来看,感觉这种方式更多的还是管理单例的工具类来使用,并且放入存储的对象不能太大,因为这些资源都是在程序开始的使用初始化。


单例模式的总结

模式总结

不管哪种形式实现单例模式,它们的核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取的过程中必须保证线程安全、防止反序列化导致重新生成实例对象等问题。

而选择哪种实现方式取决于项目本身,如是否是复杂的并发环境JDK版本是否过低单例对象的资源消耗等。

DCL失效问题

在 JDK1.5之前,DCL实现单例一直是这样的:

public class SingletonDCL {

    private static SingletonDCL instance;


    private SingletonDCL() {
    }

    public static SingletonDCL getInstance() {
        //第一道锁,主要是为了避免不必要的同步
        if (instance == null) {
            synchronized (SingletonDCL.class) {
                //第二道锁为了在null的情况下,创建实例.
                if (instance == null) {
                    instance = new SingletonDCL();
                }
            }
        }
        return instance;
    }
}

DCL模式下的单例,亮点都在 getInstance 方法中对 instance 进行了两次判空:

第一层判断主要是为了避免不必要的同步,第二层的判断则是为了在 null 的情况下创建实例。

在不涉及到并发,多线程的情况下,上述 DCL的模式是一种很好的选择,然后在高并发,多线程的情况下,DCL单例就会出现问题。

sInstance = new SingletonDCL 分析

上述代码表述的是生成一个实例的代码,但是其不是一个原子操作。这句代码最终会被编译成多条汇编指令,它大致做了3件事情:

(1)给 SingletonDCL的实例分配内存

(2)调用 Singleton()的构造函数,初始化成员字段

(3)将 sIntance 对象指向分配的内存空间(此时 sInstance就不是 null 了)

但是,由于 Java 编译器允许处理器乱序执行,以及 JDK1.5之前 JVM中 Cache、寄存器到主内存回写顺序的规定,上面第二条和第三条是无法保证顺序的。

就是执行顺序可能出现两种结果:

(1) -> (2) -> (3) 正常情况

这里写图片描述

(1) -> (3) -> (2) DCL失效情况

这里写图片描述

如果是第二种顺序,并且在3执行完毕,2未执行之前被切换到 B 线程上,这时候 sIntance 因为在线程 A 内执行过了第三点,sInstance 已经非空了,所以,线程 B 直接取走 sInstance,再使用时就会出错,这就是 DCL失效问题。

而且这种错误,难以重现和难以追踪。

在 JDK1.5之后,SUN官方已经注意到这种问题,调整了 JVM、具体优化了 volatile 关键字,如果是 JDK1.5之后的版本,在定义 Instance 时加上 volatile 关键字(用来保证 JVM生成该变量的顺序),就不会发生 DCL失效的问题。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值