单例模式

86 阅读5分钟

基本概念

单例模式,是一种常用的软件设计模式,属于创建型模式的一种。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。

应用场景

  • 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如日志文件,应用配置等。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
  • 控制资源的情况下,方便资源之间的互相通信。如线程池等。
  • 外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。
  • 内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件。

实现思路

一个类能返回一个对象引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名 称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们 还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

优缺点

优点

  • 提供了对唯一实例的受控访问。
  • 避免对共享资源的多重占用
  • 节约系统资源

缺点

  • 职责过重,在一定程度上违背了“单一职责原则”。
  • 扩展困难

实现方式

懒汉式

指全局的单例实例在第一次被使用时构建。

饿汉式

指全局的单例实例在类装载时构建。

具体实现

饿汉式实现

1.饿汉式

在类初始化时,已经自行实例化。

  public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
  
    // Private constructor suppresses
    // default public constructor
    private Singleton() {};
 
    public static Singleton getInstance() {
        return INSTANCE;
    }
  }

2.枚举式(推荐)

创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)。

public class Singleton {
    public static void main(String[] args) {
        Single single = Single.SINGLE;
    }

    enum Single {
        SINGLE;
        }
  }

懒汉式实现

1.适用于单线程

在第一次调用的时候进行实例化。

 public class Singleton {
    private static volatile Singleton INSTANCE = null;
  
    // Private constructor suppresses 
    // default public constructor
    private Singleton() {};
  
    public static  Singleton getInstance() {
         if (null == instance) {
            instance = new Singleton1();
        }
        return instance;
    }
  }

2.适用于多线程(效率不高)

虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效.因为在任何时候只能有一个线程调用getInstance()方法.但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时,而访问锁修饰的方法开销太大,性能低下。

public class Singleton{
  
  private static final Singleton instance = null;
  
  private Singleton(){}

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

3.双重检验锁DCL(推荐)

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;  
    }  
}  
  1. 第一次判空是为了在对象创建后直接返回对象,不用进入同步块内,不仅减小锁粒度,还提升了性能。
  2. synchronized同步块是为了保证只有一个线程能进入创建单例对象。
  3. 同步块内第二次判空也是为了保证只有一个对象被创建,当对象不为空时直接返回即可。
  4. volatile关键字是为了解决指令重排问题

分析:在创建一个新的对象时,JVM会做以下三件事:1.给instance分配内存 2.调用Singleton构造完成初始化 3.使instance对象的引用指向分配的内存空间(完成这一步instance就不是null了)。但由于JVM的即时编译器中存在指令重排序的优化.也就是说上面的第二步和第三步的顺序是不能保证的。可能执行顺序是 1-3-2,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

4.静态内部类(推荐)

使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

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