单例模式

定义

所谓单例就是一个类有以下特点:

  • 只允许被创建一个对象
  • 提供了一个全局访问点
  • 这个对象必须有类自己创建

应用场景

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

你可能感兴趣的:(单例模式)