定义
所谓单例就是一个类有以下特点:
- 只允许被创建一个对象
- 提供了一个全局访问点
- 这个对象必须有类自己创建
应用场景
- 表示全局唯一的类:序列号生成器、保存某些固定信息的类(比如说配置文件、密钥)
- 处理访问资源的冲突:对文件进行写操作的类
单例实现方式
饿汉式
饿汉式是在类加载的时候就创建了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));
}
}