【设计模式系列】- 单例模式

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」。

简介

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升整体的代码的性能。

特点

  1. 单例类只能有一个实例。

  2. 单例类必须自己创建自己的唯一实例。

  3. 单例类必须给所有其他对象提供这一实例。

优缺点

  1. 优点:在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。

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

实现思路

创建一个类,将其默认构造方法私有化,使外界不能通过new Object来获取对象实例,同时提供一个对外获取对象唯一实例的方法。

例如,创建一个SingleObject,如下:

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!");
   }
}
复制代码

从 singleton 类获取唯一的对象

public static void main(String[] args) {
    //不合法的构造函数
    //编译时错误:构造函数 SingleObject() 是不可见的
    //SingleObject object = new SingleObject();

    //获取唯一可用的对象
    SingleObject object = SingleObject.getInstance();

    //显示消息
    object.showMessage();
}
复制代码

执行程序,输出结果:

Hello World! 
复制代码

实现

懒汉式,线程不安全

public class Singleton {  

    private static Singleton instance;  
    
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
    	return instance;  
    }  
}
复制代码

这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。 这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

懒汉式,线程安全

public class Singleton {  

    private static Singleton instance;  
    
    private Singleton (){}  
    
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
    	return instance;  
    }  
}
复制代码

这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。

优点:第一次调用才初始化,避免内存浪费。

缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。

getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

饿汉式

public class Singleton {  

    private static Singleton instance = new Singleton();  
    
    private Singleton (){}  
    
    public static Singleton getInstance() {  
    	return instance;  
    }  
}
复制代码

这种方式比较常用,但容易产生垃圾对象。

优点:没有加锁,执行效率会提高。

缺点:类加载时就初始化,浪费内存。

它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

双重校验锁式

singleton 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 singleton 没有被实例化时,才需要进行加锁。

双重校验锁先判断 singleton 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。

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;  
    }  
}
复制代码

考虑下面的实现,也就是只使用了一个 if 语句。在 singleton == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 singleton = new Singleton(); 这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句:第一个 if 语句用来避免 singleton 已经被实例化之后的加锁操作,而第二个 if 语句进行了加锁,所以只能有一个线程进入,就不会出现 singleton == null 时两个线程同时进行实例化操作。

if (singleton == null) {
    synchronized (Singleton.class) {
        singleton = new Singleton();
    }
}
复制代码

singleton 采用 volatile 关键字修饰也是很有必要的, singleton = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 singleton 分配内存空间
  2. 初始化 singleton
  3. 将 singleton 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getSingleton() 后发现 singleton 不为空,因此返回 singleton ,但此时 singleton 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

静态内部类

public class Singleton {  
    private static class SingletonHolder {  
    	private static final Singleton INSTANCE = new Singleton();  
    }  
    
    private Singleton (){}  
    
    public static final Singleton getInstance() {  
    	return SingletonHolder.INSTANCE;  
    }  
}
复制代码

当 Singleton 类被加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getInstance() 方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。

这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。

枚举式

public enum Singleton {

    INSTANCE;

    private String objName;


    public String getObjName() {
        return objName;
    }

    public void setObjName(String objName) {
        this.objName = objName;
    }

    public static void main(String[] args) {
        // 单例测试
        Singleton firstSingleton = Singleton.INSTANCE;
        firstSingleton.setObjName("firstName");
        System.out.println(firstSingleton.getObjName());
        Singleton secondSingleton = Singleton.INSTANCE;
        secondSingleton.setObjName("secondName");
        System.out.println(firstSingleton.getObjName());
        System.out.println(secondSingleton.getObjName());
        // 反射获取实例测试
        try {
            Singleton[] enumConstants = Singleton.class.getEnumConstants();
            for (Singleton enumConstant : enumConstants) {
                System.out.println(enumConstant.getObjName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码
firstName
secondName
secondName
secondName
复制代码

该实现可以防止反射攻击。在其它实现中,通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象,如果要防止这种攻击,需要在构造函数中添加防止多次实例化的代码。该实现是由 JVM 保证只会实例化一次,因此不会出现上述的反射攻击。

该实现在多次序列化和序列化之后,不会得到多个实例。而其它实现需要使用 transient 修饰所有字段,并且实现序列化和反序列化的方法。

总结

一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。

Supongo que te gusta

Origin juejin.im/post/7065857930385424421
Recomendado
Clasificación