zoukankan      html  css  js  c++  java
  • 【设计模式(二)】单例模式

    个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

    如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充


    前言

    单例模式是Java最简单和常见的模式之一

    都知道Java中,类需要被实例化为对象才能使用,因而同一个类可以被实例化为多个对象,但是部分场景我们需要使用同一个对象,这就是单例模式出现的原因


    1.介绍

    使用目的:保证一个类仅有一个实例,并提供一个访问他的全局访问点

    使用时机:需要节省系统资源,或者需要在多个地方公用类里面的数据等情况

    解决问题:频繁创建和销毁实例导致资源浪费,同一个类的不同实例的数据互不公用

    实现方法:单例类通过私有构造函数创建唯一实例,并提供公共访问点给其他对象

    • 单例类只能有一个实例
    • 单例类必须自己创建自己的唯一实例
    • 单例类必须给所有其他对象提供这一实例

    应用实例:

    • 全局管理器,如手机app里下载管理器,只能有一个管理器,负责调控真个app的下载任务
    • 命令池等公用对象,需要公用里面的数据,那么显然只能创建唯一实例并提供给其他对象
    • 消耗资源较多的实例,如后端需要发送http请求,若频繁创建client则会消耗大量资源,那么不妨将该实例作为单例,每次都是用同一个client

    优点

    1. 仅有一个实例,那么避免了频繁创建和销毁带来的资源消耗
    2. 实例公用,那么可以保证不同对象可以使用同一份数据
    3. 实例由类自身持有,那么不会因为无人持有而销毁,从而达到数据持久化的目的

    缺点:没有接口,不能继承,自己创建自己的实例,与单一职责原则冲突(一个类应该只关心内部逻辑,而不关心外面怎么样来实例化)


    2.实现

    2.1.基本步骤

    1. 定义私有构造函数,那么该类将不会被其他对象实例化

         private SingleObject(){}
      
    2. 自己创建实例化对象

         private static SingleObject instance = new SingleObject();
      
    3. 定义全局访问点,提供给其他对象

         public static SingleObject getInstance(){
            return instance;
         }
      
    4. 定义类相关业务代码

         public void showMessage(){
            System.out.println("Hello World!");
         }
      

    完整代码

    public class SingleObject {
     
       //创建 SingleObject 的一个对象
       private static SingleObject instance = new SingleObject();
     
       //让构造函数为 private,这样该类就不会被实例化
       private SingleObject(){}
     
       //获取唯一可用的对象
       public static SingleObject getInstance(){
          return instance;
       }
     
       public void showMessage(){
          System.out.println("Hello World!");
       }
    }
    
    1. 在其他对象中获取实例,并调用相关业务方法

      public class SingletonPatternDemo {
         public static void main(String[] args) {
       
            //不合法的构造函数
            //编译时错误:私有构造函数 SingleObject() 不可见的
            //SingleObject object = new SingleObject();
       
            //获取唯一可用的对象
            SingleObject object = SingleObject.getInstance();
       
            //显示消息
            object.showMessage();
         }
      }
      

    2.2.五种常见实现方案

    2.2.1.懒汉式

    第一次被调用时初始化,节省资源,但线程不安全,

    基于“懒“的思想实现,即被动初始化,如果不被使用就不会被初始化

    私有构造器和一个公有静态工厂方法构成,在工厂方法中对singleton进行null判断,如果是null就new一个出来,最后返回singleton对象

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

    该方法的优点是节省了资源,如果不需要被使用,就不会初始化,只有第一次使用才会生成实体对象

    而缺点很明显,就是线程不安全,如果两个方法同时调用Singleton.getInstance(),就可能重复生成对象

    如果对Singleton.getInstance()加上同步锁即可解决线程不安全的问题,但是代价是每次调用都会产生不必要的同步开销,反而相当浪费资源


    2.2.2.饿汉式

    类加载时初始化,最简单常用,线程安全,但容易浪费资源

    基于“饥饿”的思想实现,即主动初始化,无论自己是否真的会被使用

    public class Singleton {  
        private static Singleton instance = new Singleton();  
        private Singleton (){}
        
        public static Singleton getInstance() {  
            return instance;  
        }  
    }
    

    优点是线程安全,且获取对象的速度很快

    但类加载时初始化无疑会拖慢项目的启动速度,而且自身并不一定会被使用,可能导致资源浪费


    2.2.3.双重锁模式

    第一次被调用时初始化,线程安全,且在多线程下依然性能优秀

    基于“双重检查”机制

    • 检查是否已被初始化,没有就创建一个实体对象,避免重复创建实例
    • 创建对象时,对类本身进行加锁同步,防止多线程下的重复创建实例

    由于singleton=new Singleton()的创建可能被JVM重排序,而导致多线程下的风险,因而使用volatile修饰signleton实例变量

    public class Singleton {  
        private volatile static Singleton singleton;  
        private Singleton (){}  
        
        public static Singleton getSingleton() {  
            if (singleton == null) {  
                synchronized (Singleton.class) {  
                    if (singleton == null) {  
                        singleton = new Singleton();  
                    }  
                }  
            }  
        return singleton;  
        }  
    }
    

    线程安全,存取较快,且在多线程下依然不影响性能,可以说是性能最好的一种方法了

    但是volatile关键词不可避免的影响了部分性能,但这一点点代价是值得的


    2.2.4.静态内部类

    第一次被调用时初始化,线程安全,且性能优秀

    使用一个静态内部类来持有自身实力,因而可以保证实体类的唯一性和线程安全性

    public class Singleton { 
        private Singleton(){
        }
        public static Singleton getInstance(){  
            return Inner.instance;  
        }  
        private static class Inner {  
            private static final Singleton instance = new Singleton();  
        }  
    } 
    

    因为是静态的内部类,所以不用担心多线程问题,且静态类必定唯一


    2.2.5.枚举模式

    类加载时初始化,简单,线程安全,且能够防止反射入侵和反序列化

    默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。实际上

    • 枚举类隐藏了私有的构造器。
    • 枚举类的域 是相应类型的一个实例对象
    public enum Singleton {
        INSTANCE;
    
        //可以省略此方法,通过Singleton.INSTANCE进行操作
        public static Singleton getInstance() {
            return Singleton.INSTANCE;
        }
    }
    

    枚举模式算是最简单的一种单例模式,但也因此导致可读性较差,实际开发中使用较少

    但其防反射入侵和反序列化的特性,受到很多人的推崇,号称最安全实用的单例模式???


    3.补充

    3.1.双重锁模式与volatile

    对于双重锁模式的创建方法

        public static Singleton getSingleton() {  
            if (singleton == null) {  
                synchronized (Singleton.class) {  
                    if (singleton == null) {  
                        singleton = new Singleton();  
                    }  
                }  
            }  
        return singleton;  
        }  
    

    第5行的singleton = new Singleton();实际上包括以下三步

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

    由于2和3之间没有依赖关系,其顺序可能会被JVM重排序,而变成下面的顺序

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

    在单线程下不会出现异常,如对于线程A,A2和A3互换顺序并不影响结果,最后得到的结果都是初始化后的对象

    但如果在多线程情况下,线程A的A2与A3互换位置,致使某个瞬间instance不为空,但并没有初始化对象

    此时另一个线程B将会获得一个还未初始化完成的instance,那么就喜闻乐见的空指针了

    解决办法就是把instance声明为volatile型,因为被volatile关键字修饰的变量是被禁止重排序的


    3.2.单例模式被破坏

    单例模式的主要目的就是保持自身仅有一个实例对象,那如果出现了多个实例对象呢?这个模式就被破坏了

    上述几种实现方法中,我们都隐藏了构造函数,而仅仅暴露一个公共入口,用这个入口来保持仅有一个实例对象

    那么只需要跳过这个入口,直接访问构造函数,或者直接访问实例对象,就相当于破坏了这个模式

    常见情况有两种

    • 映射:隐射能获取一个对象里的所有变量和属性,即便是私有的,那么可以将类进行复制,并将私有构造函数修改为共有的,就可以利用构造函数生成新的实例了,举例如下

      单例使用饿汉式单例,测试方法如下

      package com.company.test;
      
      import java.lang.reflect.Constructor;
      
      public class SingletonTest {
          private static SingletonTest instance = new SingletonTest();
      
          private SingletonTest() {
          }
      
          public void saySomething() {
              System.out.println("Hello world!");
          }
      
          public static SingletonTest getInstance() {
              return instance;
          }
      
          public static void main(String[] args) throws Exception {
              SingletonTest s1 = SingletonTest.getInstance();
              SingletonTest s2 = SingletonTest.getInstance();
              Constructor<SingletonTest> constructor = SingletonTest.class.getDeclaredConstructor();
              constructor.setAccessible(true);
              SingletonTest s3 = constructor.newInstance();
              System.out.println("s1:" + s1 + "
      " + "s2:" + s2 + "
      " + "s3:" + s3);
              System.out.println("正常情况下,实例化两个实例是否相同:" + (s1 == s2));
              System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (s1 == s3));
          }
      }
      
      

      image-20200928172252537

      可以看到两个通过实例提供的公共接入口生成的是同一个实例,而映射复制后生成的并不是同一个

    • 序列化:即将对象转换成字节序列,再将其转换回对象,那么转回的对象与原对象是否是同一个呢?显然不是,这样就破坏了单例模式的原则了

      依旧使用单例模式,测试方法如下

      package com.company.test;
      
      import java.io.*;
      import java.lang.reflect.Constructor;
      
      public class SingletonTest implements Serializable {
          private static SingletonTest instance = new SingletonTest();
      
          private SingletonTest() {
          }
      
          public void saySomething() {
              System.out.println("Hello world!");
          }
      
          public static SingletonTest getInstance() {
              return instance;
          }
      
          public static void main(String[] args) throws Exception {
              SingletonTest s1 = SingletonTest.getInstance();
              ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
              oos.writeObject(s1);
              oos.flush();
              oos.close();
      
              FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
              ObjectInputStream ois = new ObjectInputStream(fis);
              SingletonTest s4 = (SingletonTest) ois.readObject();
              ois.close();
              System.out.println("s1:" + s1 + "
      " + "s4:" + s4);
              System.out.println("序列化前后两个是否同一个:" + (s1 == s4));
          }
      }
      

      image-20200928173444430

      显然,新的对象也不是旧的实例

    上述五种单例模式,都可以被映射和序列化破坏掉,但枚举模式不会


    3.3.枚举模式的防破坏机制

    上面说了,常见的破坏方法就隐射和序列化,枚举模式天生可以避免这两种情况

    • 反射在通过newInstance()创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败

      测试方法同上,单例模式改为枚举模式

      package com.company.test;
      
      import java.io.*;
      import java.lang.reflect.Constructor;
      
      public enum SingletonTest implements Serializable {
          INSTANCE;
      
          //可以省略此方法,通过Singleton.INSTANCE进行操作
          public static SingletonTest getInstance() {
              return SingletonTest.INSTANCE;
          }
      
          public static void main(String[] args) throws Exception {
              SingletonTest s1 = SingletonTest.getInstance();
              SingletonTest s2 = SingletonTest.getInstance();
              Constructor<SingletonTest> constructor = SingletonTest.class.getDeclaredConstructor();
              constructor.setAccessible(true);
              SingletonTest s3 = constructor.newInstance();
              System.out.println("s1:" + s1 + "
      " + "s2:" + s2 + "
      " + "s3:" + s3);
              System.out.println("正常情况下,实例化两个实例是否相同:" + (s1 == s2));
              System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:" + (s1 == s3));
          }
      }
      
      

      结果如下,VM抛出异常

      image-20200928174508316

    • enum类不能被继承,在反编译的时候可以发现该类是final的,而且enum类有且仅有private的构造器,防止被外部构造

      因此可以保证对于序列化和反序列化,每一个枚举类型和枚举变量在JVM中都是唯一的,因而序列化和反序列化并不会生成新的实例,也就不会破坏单例模式

      测试代码如下

      package com.company.test;
      
      import java.io.*;
      
      public enum SingletonTest implements Serializable {
          INSTANCE;
      
          //可以省略此方法,通过Singleton.INSTANCE进行操作
          public static SingletonTest getInstance() {
              return SingletonTest.INSTANCE;
          }
      
          public static void main(String[] args) throws Exception {
              SingletonTest s1 = SingletonTest.getInstance();
              ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
              oos.writeObject(s1);
              oos.flush();
              oos.close();
      
              FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
              ObjectInputStream ois = new ObjectInputStream(fis);
              SingletonTest s4 = (SingletonTest) ois.readObject();
              ois.close();
              System.out.println("s1:" + s1 + "
      " + "s4:" + s4);
              System.out.println("序列化前后两个是否同一个:" + (s1 == s4));
          }
      }
      

      image-20200928181429936

      可以看到s1和s4都是INSTANCE,是一个唯一常量,序列化和反序列化并没有生成新的实例


    4.传送门

    https://www.cnblogs.com/chiclee/p/9097772.html

    https://www.cnblogs.com/saoyou/p/11087462.html


    5.后记

    对于五种实现方案,饿汉式是最常用的,而枚举是最被推崇的模式,但个人还是比较喜欢双重锁模式,看情况使用吧,还是不能无视实际场景一慨而论


    作者:Echo_Ye

    WX:Echo_YeZ

    Email :echo_yezi@qq.com

    个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

  • 相关阅读:
    SVN
    git
    电商架构
    django
    linux单项目发布流程
    pandas的基本功能(一)
    Swift 添加自定义响应事件
    Swfit中视图跳转
    移动设备默认不播放媒体文件间接解决办法
    HTML5 使用sessionStorage实现页面返回刷新
  • 原文地址:https://www.cnblogs.com/silent-bug/p/13746205.html
Copyright © 2011-2022 走看看