美文网首页
应用最广的模式——单例模式

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

作者: 熊sir要早睡早起 | 来源:发表于2018-02-17 23:35 被阅读0次

    单例模式的简单介绍:
    单例模式是应用最广的模式之一,在应用这个模式时,单列对象的类必须保证只有一个实例的存在,许多时候整个系统只需要拥有一个全局对象,这有利于余我们协调系统的整体行为。在Android当中,对于Retrofit的使用者常常会写一个Retrofit的单例,来确保整个APP只有一个Retrofit的实例,再比如ImageLoader,可能含有线程池、缓存、网络请求等等,非常消耗系统资源,同样也没理由构建多个实例,还有我们经常使用到的Toast,重复点击易出现不友好的交互,这些情况都是单例模式的使用场景。

    单例模式的定义:
    确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

    实现单例模式的几个关键点:
    (1)构造函数一般私有化,不对外开放
    (2)通过一个静态方法或者枚举返回单列类对象
    (3)确保单例类对象有且只有一个,尤其是在多线程环境下
    (4)确保单例类对象在反序列化时不会重新构建对象。

    单例模式的简单示例:
    单例模式在设计模式中比较简单,只有一个单例类,没有其他的层次结构与抽象。该模式需要确保该类只能生成一个对象,通常时该类需要消耗比较多的资源或者没有多个实例的情况。例如,一个学校只有一个校长,一个应用程序员只有一个Application对象等。以学校有一个校长为例:下面看代码体现。

    首先创建一个Teacher类

    /**
     * 学校有很多普通老师
     */
      public class Teacher {
    
        public void teach(){
        //教学
    }
    }
    

    然后创建一个校长类,这里注意使用饿汉单例:

      /**
       * 校长,一个学校应该只有一个校长
     * 此处使用饿汉单例模式
     */
    public class Principal extends Teacher{
    
    private static final Principal pal = new Principal();
    //构造方法私有化
    private Principal(){};
    //共有的静态函数,对外暴露获取单列对象的接口
    public static Principal getPal(){
        return pal;
    }
    }
    

    最后我们写一个测试类:

    import java.util.ArrayList;
    import java.util.List;
    
    public class Test {
    
    public static void main(String[] args) {
    
        List<Teacher> list = new ArrayList<>();
    
        //创建两个老师
        Teacher teacher1 = new Teacher();
        Teacher teacher2 = new Teacher();
        //通过暴露的getPal方法创建两个校长
        Principal principal1 =  Principal.getPal();
        Principal principal2 = Principal.getPal();
    
        list.add(teacher1);
        list.add(teacher2);
        list.add(principal1);
        list.add(principal2);
        //遍历集合,查看输出对象
        for (Teacher tech : list){
            System.out.println("Obj : "+tech.toString());
        }
    }
    
    }
    

    运行程序查看控制台输出:


    饿汉.png

    使用了单例模式的校长,虽然调用两次getPal,但实际上只创建了一个校长。因为getPal方法返回的都是我们Principal类中已经创建好的校长啦,我们把构造方法私有化也是为了避免使用时误操作通过new直接创建了对象,这样就使得程序不仅仅有一个校长了,与我们单例的初衷所违背。正如其名,饿汉式在代码中也有体现,我们的校长是一个静态对象,在声明的时候已经初始化了,这样我们调用getPal时才保证了校长的唯一性。

    单例模式的其它实现:
    懒汉模式:懒汉模式是声明一个静态对象,并且在用户第一次调用getInstance时进行初始化,而上述的饿汉模式时在声明静态对象时就已经初始化。懒汉单例模式的实现如下。

    /**
     * 懒汉单例模式
     */
    public class SingletonPal {
    
    private static SingletonPal instance;
    //私有化构造犯法
    private SingletonPal (){};
    
    public static synchronized SingletonPal getInstance(){
        if (instance==null){
            instance = new SingletonPal();
        }
        return instance;
    }
    }
    

    同样的,我们运行测试类观察结果:


    懒汉.png

    同样的实现了校长的唯一性。

    那么两者的区别是什么呢:
    细心的读者发现了getInstance()方法中添加了同步锁,也就是说这是一个同步方法,每次调用getInstance方法都会进行同步,这样会造成不必要的同步开销,这也是懒汉单例模式存在的最大问题。而它的优点是只有在使用时才会被实例化,在一定成都上节约了资源。此模式一般不建议使用

    有没有其它方式实现单例呢?
    DCL方式:Double Check Lock方式实现单例模式的优点是既能够在需要时才初始化单例,又能保证线程安全,且单例对象初始化后调用getInstance不进行同步锁。代码如下所示:

    /**
     * DCL单例模式
     */
    public class DCLPal {
    
    private volatile static DCLPal instance = null;
    //私有化构造方法
    private DCLPal(){};
    public static DCLPal getInstance(){
        if (instance==null){
            synchronized (DCLPal.class){
                if (instance==null){
                    instance = new DCLPal();
                }
            }
        }
        return instance;
    }
    }
    

    运行结果如下:


    DCL.png

    此模式的亮点也在getInstance方法上,可以看到getInstance方法中对instance进行了两次为空判断,第一次时为了避免不必要的同步,第二层是在为空的情况下创建实例。

    其实此处也有一个bug,那就是instance = new DCLPal语句虽然是一句代码,但实际上这并不是一个原子操作(原子操作是指不会被线程调度打断的操作,这种操作一旦开始就会一直运行到结束。。原子操作(atomic operation)是不需要synchronized),这句代码最终会被编译成多条汇编指令,它大致做了三件事情:
    (1)给DCLPal的实例分配内存
    (2)调用DCLPal()构造函数,初始化成员字段
    (3)将instance对象指向分配的内存空间(此时instance就不为null了)
    But,由于Java编译器运行处理器乱序执行,以及JDK1.5之前的JMM(Java内存模型)中cache、寄存器到主内存回写顺序的规定,第二步和第三步的顺序无法保证,如果遇到执行了1-3之后,2还没有执行就被切换到另外一个线程当中,此时我们的instance已经是非null了,所以线程B直接取走了instance,使用时就会出现报错,这就是DCL失效。
    在JDK1.5之后,SUN官方已经注意到了此问题,所以调整了JVM,具体化了volatile关键字,因此,如果是JDK1.5或之后的版本,只需要在private 后面加上volatile关键字来保证instance对象每次都是从主内存中读取,这样就可以使用DCL的方式来实现单例。此方式一般能够满足需求

    静态内部类单例模式:
    DCL虽然在一定程度上解决了咨询员消耗,多余的同步,线程安全等问题,但是在某些情况下仍然会出现失效的问题。这个问题被称为双重检查锁定失效,在《Java并发编程》中指出这种“优化”是丑陋的,不赞成使用,而是建议使用如下的代码代替。

    /**
     * 匿名内部类实现单例
     */
    public class InnerClassPal extends Teacher{
    //私有化构造方法
    private InnerClassPal(){};
    
    public static InnerClassPal getInstance(){
        return PalHolder.instance;
    }
    
    private static class PalHolder{
        private static final InnerClassPal instance = new InnerClassPal();
    }
    }
    

    运行结果如下:


    匿名内部类.png

    当第一次加载InnerClassPal类时并不会初始化instance,只有在第一次调用InnerClassPal的getInsatnce方法时才会导致instance被初始化,因此,第一次调用getInstance方法会导致虚拟机加载InnerClassPal类,因为在多线程环境下,jvm对一个类的初始化会做限制,同一时间只会允许一个线程去初始化一个类,这种方式不仅能保证线程安全,也能够保证单例对象的唯一性,同时也延迟了单例子的实例化,所以这是最推荐使用的单例模式的实现方法。

    枚举单例
    枚举单例是最简单的实现方式,因为枚举在Java中与普通的类是一样的,不仅能够又字段,还能够拥有直接的方法,最重要的是枚举实例的创建是线程安全的,在任何情况下都是一个单例。(以上几种情况在反序列化的情况下都会重新创建对象。。如果需要杜绝单例对象在被反序列化时重新生成对象,必须加入readResolve函数)

    首先是枚举类型代码实现:

    class Pal{
    
    public void teach(){
        System.out.println("教书");
    }
    
    }
    
    public enum  PalEnum {
    
    INSTANCE;
    
    private Pal instance;
    
    PalEnum(){
        instance = new Pal();
    }
    
    public Pal getInstance() {
        return instance;
    }
    }
    

    运行结果如下:


    枚举.png

    readResolve避免反序列化重新生成对象:

    /**
     * 添加readResolve方法避免反序列化后重复创建对象
     */
    public class Singleton implements Serializable{
    
    private static final long serialVersionUID = 0L;
    private static final Singleton INSTANCE = new Singleton();
    
    //私有化构造方法
    private Singleton(){}
    
    public static Singleton getInstance(){
        return INSTANCE;
    }
    
    private Object readResolve() throws ObjectStreamException{
        return INSTANCE;
    }
    
    }
    

    使用容器实现单例模式:
    在学习了上述各类单例的实现之后,再来看看一种另类的实现,具体代码如下:

    public class SingletonManager {
    
    private static Map<String,Object> objMap = new HashMap<>();
    //私有化构造方法
    private SingletonManager(){};
    
    public static void registerService(String key ,Object instance){
        if (!objMap.containsKey(key)){
            objMap.put(key,instance);
        }
    }
    
    public static Object getService(String key){
        return objMap.get(key);
    }
    }
    

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

    总结:
    不管以那种形式实现的单例模式,它们的核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取过程中必须保证线程安全,防止反序列化导致重新生成实例对象等问题。选择哪种实现方式取决于项目本身,如是否复杂的并发环境、JDK版本是否过低、单例对象的资源消耗等问题。

    原文出自Android源码设计模式解析与实战一书。

    相关文章

      网友评论

          本文标题:应用最广的模式——单例模式

          本文链接:https://www.haomeiwen.com/subject/ossmtftx.html