【设计模式(二)】单例模式 前言 1.介绍 2.实现 3.补充 4.传送门 5.后记

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

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


单例模式是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));
        }
    }
    
    

    【设计模式(二)】单例模式
前言
1.介绍
2.实现
3.补充
4.传送门
5.后记

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

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

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

    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));
        }
    }
    

    【设计模式(二)】单例模式
前言
1.介绍
2.实现
3.补充
4.传送门
5.后记

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

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


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抛出异常

    【设计模式(二)】单例模式
前言
1.介绍
2.实现
3.补充
4.传送门
5.后记

  • 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));
        }
    }
    

    【设计模式(二)】单例模式
前言
1.介绍
2.实现
3.补充
4.传送门
5.后记

    可以看到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

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