首页 > 代码库 > 单例模式详解

单例模式详解

1. 单例模式简介

  单例模式是一种对象创建模式,它用于产生一个对象的具体实例,并确保系统中有且只有该对象的实例。

  Java 语言中的单例模式是一个虚拟机范围,因为装载类的功能是虚拟机提供的,所以一个虚拟机实例在通过自己的 ClassLoad 装载实现单例模式的类的时候,只会创建一个类的实例

单例模式的核心在于通过一个接口返回唯一的对象实例,并排除其他外部能够创建新对象的可能。那么,首要的问题就是把创建对象的权限回收,让对象自身来负责其实例的创建工作,然后通过该对象提供的外部可访问接口获取该对象的实例。

  • 一般在两种场景下会考虑使用单例(Singleton)模式:
  1. 产生某对象会消耗过多的资源,为避免频繁地创建与销毁对象对资源的浪费。如:

    对数据库的操作、访问IO、线程池(threadpool)、网络请求等。如对配置文件的读取。

  2. 某种类型的对象应该有且只有一个。如果制造出多个这样的实例,可能导致:程序行为异常、资源使用过量、结果不一致等问题。如果多人能同时操作一个文件,又不进行版本管理,必然会有的修改被覆盖,所以:

    一个系统只能有:一个窗口管理器或文件系统,计时工具或 ID(序号)生成器,缓存(cache),处理偏好设置和注册表(registry)的对象,日志对象。

  • 单例模式的优点:可以减少系统内存开支,减少系统性能开销,减轻垃圾回收的压力,同时还能避免对资源的多重占用、同时操作。
  • 单例模式的缺点:扩展很困难,容易引发内存泄露,测试困难,一定程度上违背了单一职责原则,进程被杀时可能有状态不一致问题。

2. 单例模式的实现

  我们经常看到的单例模式,按加载时机可以分为:饿汉方式和懒汉方式;按实现的方式,有:双重检查加锁,内部类方式和枚举方式等等。另外还有一种通过Map容器来管理单例的方式。它们有的效率很高,有的节省内存,有的实现得简单漂亮,还有的则存在严重缺陷,它们大部分使用的时候都有限制条件。下面我们来分析下各种写法的区别,辨别出哪些是不可行的,哪些是推荐的,最后为大家筛选出几个最值得我们适时应用到项目中的实现方式。

  因为下面要讨论的单例写法比较多,筛选过程略长,结论先行:
  无论以哪种形式实现单例模式,本质都是使单例类的构造函数对其他类不可见,仅提供获取唯一一个实例的静态方法,必须保证这个获取实例的方法是线程安全的,并防止反序列化、反射、克隆(多个类加载器、分布式系统)等多种情况下重新生成新的实例对象。至于选择哪种实现方式则取决于项目自身情况,如:是否是复杂的高并发环境、JDK 是哪个版本的、对单例对象资源消耗的要求等。

技术分享

  • 上表中仅列举那些线程安全的实现方式,永远不要使用线程不安全的单例!
  • 另有使用容器管理单例的方式,属于特殊的应用情况,下文单独讨论。

直观一点,再上一张图:

技术分享

  • 此四种单例实现方式都是线程安全的,是实现单例时不错的选择

下面就来具体谈一下各种单例实现方式及适用范围。

饿汉式

  一种简单的单例实现方案如下:

public class Singleton {

    private static Singleton singleton = new Singleton();

    private Singleton() {
        // ToDo  do somthing here
    }

    public static Singleton getInstance() {
        return singleton;
    }
}

  Singleton 类的构造函数被声明为 private,确保了其不能在类的外部通过 new 关键字创建新的对象。也就是说,这样使得该单例类不会在系统的其他代码中被实例化

  但是,由于使用了 static 作为字段 singleton 的修饰符,那么,在 Java 虚拟机加载 Singleton 类时就会初始化 singleton 对象。如果 Singleton 在系统中还扮演其他角色,那么在系统中任何使用这个单例类的地方都会初始化 singleton 对象,无论此时 singleton 对象是否被使用到:

public class Singleton {

    private static Singleton singleton = new Singleton();

    private Singleton() {
        // ToDo  do somthing here
        System.out.println("Singleton is create");
    }

    public static Singleton getInstance() {
        return singleton;
    }

    public static void printSomething() {
        System.out.println("print in Singleton");
    }

    public static void main(String[] args) {
        Singleton.printSomething();
    }
}

  程序输出:

Singleton is create
print in Singleton

  也就是说,该方法创建的单例类会在第一次引用该类的时候就创建对象实例,而不管实际是否需要。这样写的好处在于结构简单,但是达不到按需加载的目的

懒汉式(线程不安全

  为了解决上述单例类无法按需加载的问题,我们对上述代码做如下改动:

public class Singleton {

    private static Singleton singleton = null;

    private Singleton() {
        // ToDo  do somthing here
    }

    public static synchronized Singleton getInstance() {
        return singleton == null ? singleton = new Singleton() : singleton;
    }
}

  代码中对静态成员变量 singleton 初始化赋值 null,确保类加载时没有额外的负载,然后在获取 Singleton 对象实例的时候(getInstance())通过 singleton == null ? singleton = new Singleton() : singleton; 实现 singleton对象的按需加载。

  但是此时如果有多个线程对 getInstance() 方法进行调用,那么由于线程的同步问题,那么很有可能会出现重复创建 Singleton 对象的情况。这种极端情况的出现是由于,在多线程环境中,如果线程 A 调用 getInstance() 方法获取对象实例,Singleton 对象正在创建,而此时 线程 B 也恰巧调用 getInstance(),那么在 线程 B 中可能会判断 singleton对象为 null,也会创建一个新的 Singleton 对象。

  所以,在多线程环境中,一种更为严格的单例模式应该是线程安全的:

双重校验锁

public class Singleton {

    private static volatile Singleton singleton = null;

    private Singleton() {
        // ToDo  do somthing here
    }

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

  上面这种写法被称为 双重检查锁,也就是说,在 getInstance() 方法中进行了两次 null 判断。如此一来,在对 Singleton.class 对象加锁前进行一次 null 判断可以避免绝大多数的加锁操作,进而提升代码的执行效率。

  但是,由于在 JDK 1.5 之前的版本中,由于对 禁止指令重排优化 并不支持(volatile 关键字确保编译器禁止指令重排优化),所以 双重检查锁 的单例模式也是无法保证线程安全的。

  在多线程环境中,由于编译器只保证程序执行结果与源代码相同,但是不保证实际指令的执行顺序与源代码相同。所以会出现在多线程环境下调用 getInstance() 方法时会产生乱序的问题。这就是编译器对指令做了 指令重排序优化 造成的。而 volatile 关键字的作用则是通知编译器不执行 指令重排序优化。

  singleton = new Singleton(); 是一条简单的初始化并赋值的操作,但是它不是一个原子操作。对于编译器来说,执行该行代码大致需要做三件事情:
  A:为对象分配内存;
  B:调用 Singleton 的构造函数初始化对象;
  C: 将引用 singleton 指向新分配的内存空间

  那么,操作 B 依赖于 操作 A,操作 C 依赖于操作 A,对于便一起来说,操作 B 与操作 C 是相互独立的,是可以被重新优化排序的。

静态内部类单例模式

  双重检查锁 存在的问题的直接原因是初始化一个对象并使一个引用指向它的过程并不是 volatile的,导致了可能会出现引用指向了对象并没有初始化完成的那块堆内存。即便是在 JDK 1.5之后可以通过使用 volatile 关键字解决问题,但是我们可以通过更为优雅,效率更高的方式实现单例模式:

public class Singleton {

    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

 

单例模式详解