单例模式

  • 定义
  • 应用场景
  • 单例实现方式
    • 饿汉式
    • 懒汉式
    • 双重校验锁
    • 枚举类
    • 静态内部类
  • 单例模式扩展
    • 线程唯一的单例
    • 集群唯一实例
    • 多例模式
定义 所谓单例就是一个类有以下特点:
  • 只允许被创建一个对象
  • 提供了一个全局访问点
  • 这个对象必须有类自己创建
应用场景
  1. 表示全局唯一的类:序列号生成器、保存某些固定信息的类(比如说配置文件、密钥)
  2. 处理访问资源的冲突:对文件进行写操作的类
单例实现方式 饿汉式
饿汉式是在类加载的时候就创建了Singleton对象,即,不是需要使用的时候才创建它(不支持懒加载)是其最大的问题,容易造成资源浪费(可能性有点小哦)。
public class Singleton { // 保存该类的唯一实例 private static Singleton instance = new Singleton(); /** * 私有构造器使其他类无法直接通过new创建该类的实例 */ private Singleton() { // 什么也不做 } /** * 获取单例的主要方法 */ public static Singleton getInstance() { return instance; } }

懒汉式
下面是一个饿汉式单例的最简单实现方式,做到了需要的时候才去加载它,但是这种写法是会出现问题的,例如线程A调用getInstance发现instance == null然后它执行到2位置(假设此时对象还没new成功),此时又有线程B进入发现此时instance仍然为null也会去创建一个新的对象,这就导致线程A和线程B得到的对象不是一个对象,这就是下面代码在多线程条件下会出现的问题。
public class Singleton { // 保存该类的唯一实例 private static Singleton instance = null; /** * 私有构造器使其他类无法直接通过new创建该类的实例 */ private Singleton() { // 什么也不做 } /** * 获取单例的主要方法 */ public static Singleton getInstance() { if(instance == null){//1 instance = new Singleton(); //2 } return instance; } }

上面的问题似乎很好解决→加锁
加锁确实解决了上面的问题,但是以后每次getInstance都会多一次加锁的过程,这样不完美啊!所以就出现了下面一种实现方式双重校验锁。
public class Singleton { // 保存该类的唯一实例 private static Singleton instance = null; /** * 私有构造器使其他类无法直接通过new创建该类的实例 */ private Singleton() { // 什么也不做 } /** * 获取单例的主要方法 */ public static synchronized Singleton getInstance() { if(instance == null){//1 instance = new Singleton(); //2 } return instance; } }

双重校验锁
下面是一个经典的双重校验锁的单例实现
public class Singleton { // 保存该类的唯一实例 private static Singleton instance = null; /** * 私有构造器使其他类无法直接通过new创建该类的实例 */ private Singleton() { // 什么也不做 } /** * 获取单例的主要方法 */ public static Singleton getInstance() { if (null == instance) {// 操作1:第1次检查 synchronized (Singleton.class) { //操作2 if (null == instance) {// 操作3:第2次检查 instance = new Singleton(); // 操作4 } } } return instance; } }

首先我们分析一下为什么操作1和操作2的作用
由于上述的的问题所以在操作3之前加一个操作2这样就会保证一次只会有一个线程来执行操作4,但是,这样就会造成每次调用getInstance()都要申请/释放锁会造成极大的性能消耗,所以需要在操作2之前加一个操作1就会避免这样的问题。
另外static修饰变量保证它只会被加载一次。
这样看来这个双重校验锁就完美了?
上面的操作4可以分为以下3个子操作
objRef = allocate(IncorrectDCLSingletion.class); // 子操作1:分配对象所需的存储空间 invokeConstructor(objRef); // 子操作2:初始化objRef引用的对象 instance = objRef; // 子操作3:将对象引用写入共享变量

synchronized的临界区内是允许重排序的,JIT编译器可能把以上的操作重排序成 子操作1→子操作3→子操作 2,所以可能发生的情况是一个线程在执行到重排序后的操作4(1→3→2)的时候,线程刚执行完子操作3的时候(子操作2没有被执行),有其它的线程执行到操作1,那么此时instance ≠ null就会直接将其retuen回去,但是这个instance是没有被初始化的,所以会出现问题。
如果instance使用volatile关键字修饰,这种情况就不会发生,volatile解决了子操作2和子操作3的的重排序问题。
volatile还能避免这个共享变量不被存到寄存器当中,避免了可见性问题。
枚举类
枚举类也可以安全的实现单例模式
public class Singleton { // 私有构造器 private Singleton() { }private static class InstanceHolder { // 保存外部类的唯一实例 final static Singleton INSTANCE = new Singleton(); }public static Singleton getInstance() { return InstanceHolder.INSTANCE; } }

上面是内部静态类的实现方式,InstanceHolder会在调用的时候加载因此这也是一种懒汉式的单例。
静态内部类
由于静态变量只有在它被使用的时候才初始化,所以只有在执行1的时候才会创建Singleton实例,另外,静态内部类的加载是在程序中调用静态内部类的时候加载的,和外部类的加载没有必然关系。
public class Singleton { /** * 私有构造器使其他类无法直接通过new创建该类的实例 */ private Singleton() { // 什么也不做 } private static class SingletonHolder{ private static Singleton instance = new Singleton(); } /** * 获取单例的主要方法 */ public static synchronized Singleton getInstance() { return SingletonHolder.instance; //1 } }

单例模式扩展 线程唯一的单例
这种单例就是每个线程只能创建一个唯一的对象,可以创建一个k-v容器用于存储对象和线程之间的关系,下面代码便是实现了一个饿汉式的线程唯一单例。
public class ThreadSingleton { //类变量 private static Map threadSingletonMap = new HashMap<>(); private ThreadSingleton() { } public static ThreadSingleton getInstance() { if (!threadSingletonMap.containsKey(Thread.currentThread())){ threadSingletonMap.put(Thread.currentThread(),new ThreadSingleton()); } return threadSingletonMap.get(Thread.currentThread()); } }

除了上面的方式,使用ThreadLocal来实现会更加简便
public class ThreadLocalSingleton { private static ThreadLocal threadLocal = new ThreadLocal<>(){ @Override protected Object initialValue() { return new ThreadLocalSingleton(); } }; private ThreadLocalSingleton(){}public static ThreadLocalSingleton getInstance(){ return threadLocal.get(); } }

集群唯一实例
一般单例指的是对于同一台虚拟机而言的单例,而集群唯一单例可能涉及到多个虚拟机,所以是需要借助持久化技术来实现,例如将对象序列化后存储到数据库或者文件当中,然后再通过反序列化的方式将其转换为对象,另外,为了保证任何时刻只有一个服务拥有这个对象,所以需要加锁(分布式锁)。
多例模式
【单例模式】多例模式就是一个类只会有这么几个固定的对象,即在其它类种获取的对象肯定是其中之一。
public class MultiSingleton { private static Map multiSingletonMap = new HashMap<>(); static { multiSingletonMap.put(0,new MultiSingleton()); multiSingletonMap.put(1,new MultiSingleton()); multiSingletonMap.put(2,new MultiSingleton()); }private MultiSingleton(){ }public static MultiSingleton getInstance() { return multiSingletonMap.get(new Random().nextInt(3)); } }

    推荐阅读