1.单例模式的作用
单例模式,目的是为了保证在进程中,某个类有且仅有一个实例。
由于只有一个实例,所有调用方不能使用new关键字来创建实例。所以,单例模式的类的构造方法必须是私有的,这样就保证了调用方无法创建实例。
2.单例模式实现的三个必要条件
- 单例类的构造方法必须是私有的,这样才能保证创建实例权控制在自己内部,从而使得类的外部不能创建类的实例。
- 单例类通过一个私有的静态变量来存储唯一实例,因为如果实例存储变量是私有的,外部代码就不能直接访问或修改它。静态保证该变量是全局共享且唯一的,这就避免了创建多个实例的可能性,确保类的实例始终是唯一的。
- 单例类通过一个公开的静态方法,使外部使用者可以访问类的唯一实例。
3.实现单例模式需要考虑的三个问题
- 创建单例对象时,是否线程安全。
- 单例实例创建时,是否延迟加载。
- 获取单例对象时,是否需要加锁。
下面为大家围绕这三个问题和三个必要条件介绍几种单例实现的方式。
4.饿汉式
public class Singleton {
//这里使用private满足了只能自己使用的必要条件
//这里使用static是jvm在加载该类时,只初始化一次,确保了单例性
private static Singleton instance = new Singleton();
//使用private满足调用者不能够使用new来创建实例
private Singleton() {};
//向使用者提供了单例的使用方法
public static Singleton newInstance() {
return instance;
}
}
优点:饿汉式在jvm初始化阶段是,会执行类的静态方法,在类的初始化阶段,静态方法只会执行一次,这也保证了单例性,也没有多线程的安全问题。是线程安全的,同时因为,jvm在初始化阶段会获取class对象锁,这个锁可以同步多个线程初始化一个类,不需要加锁。
缺点:在对象序列化和反序列化时依旧会破坏单例性,没有用到也会创建实例,在类加载的时候会被创建,会造成内存浪费,而且没有懒加载那么灵活,无法实现按需加载。存在序列化和反序列化问题。
5.懒汉式
懒汉式相对于饿汉式不同之处在于是支持懒加载的,将对象创建延迟到了获取对象实例的时候,不过为了线程安全,在获取对象锁的时候需要加锁,这也就导致了性能低。
public class Singleton {
//这里依旧要保持私有变量防止外部破坏
private Singleton instance;
//这里依旧保持私有构造方法
private Singleton() {}
//这里给方法上了重量级锁,确保线程同步初始化实例
public static synchronized Singleton getInstance() {
//这里使用一次检测该变量是否为空,目的是保证该类的实例性
if (instance == null) {
instance = new Singleon();
}
return instance;
}
}
其实,这段代码还是可以优化一下,这段代码只有在第一次创建实例的时候需要锁,获取实例的时候其实是不需要锁了,而双重检测就优化了这个问题。先说说这段代码的优缺点吧。
优点:对象创建的时候是线程安全的,支持延时加载。
缺点:获取对象方法上加了重量级锁,这会大大降低性能。存在序列化和反序列化问题
解决的办法就是双重检查锁定,其中synchronized方法改成synchronized类对象,还有一点是使用到了jdk5版本之后的volatile关键字,在jdk5版本之前没有volatile关键字的时候,是存在指令重排的问题的,这部分是属于JUC的部分知识,我稍后会在JUC专栏去更新相关知识,可以多多关注!
public class Singleton {
//这点使用了volatile关键字保证了顺序性,这点我待会会重点讲解过程
//这里将变量赋值为null是保证不违背双重检测,会因为指令重排的优化问题,不赋值为null的话,在分配 空间的时候会直接赋值引用内存地址,这样就不是为null,在第一次判断是否为空的时候就违背了双重检测
private static volatile Singleton instance = null;
private Singleon() {};
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里instance使用了static修饰,是因为要确保单例模式的必要条件之一私有静态变量。
为什么两次判断instance为null,在synchronized之前是存在线程安全问题的,我画个表格解释一下过程:
Time | Thread A | Thread B |
---|---|---|
T1 | 检测到instance == null | |
T2 | 检测到instance == null | |
T3 | 初始化对象 A | |
T4 | 返回对象A,此时instance=A | |
T5 | 初始化对象为B | |
T6 | 返回对象B,此时instance=B,这样就会覆盖Thread B初始化的值 |
我再解释一下,为什么要使用volatile关键字,首先我先解释一下new Singleton()执行时的三个步骤:
memory = allocate(); // 1.分配对象的内存空间
ctorInstance(memory); // 2.初始化对象
instance = memory; // 3.设置instance指向分配的内存地址
由于指令重排优化的存在,导致对象初始化操作和引用指向内存地址的时候顺序不确定,也就是上面提到的2和3的操作的顺序不确定性,所以在线程创建单例的时候,为该对象分配内存空间并且设置字段为默认值的时候引用内存空间地址,正准备将字段初始化的时候,来了另一个线程调用geiInstance方法,发现instance不为null,这个时候就是取到的是还未初始化的值,程序就会出错,volatile可以禁止指令重排优化,保证了先初始化后分配内存地址。
Time | Thread A | Thread B |
---|---|---|
T1 | 检查instance==null | |
T2 | 获得锁 | |
T3 | 再次检查instance==null | |
T4 | 为instance分配空间 | |
T5 | 将instance指向内存空间 | |
T6 | 检测instance 不为null | |
T7 | 访问instance(此时对象还未初始化完成) | |
T8 | 初始化instance |
双重检查锁定单例优点:
- 线程安全
- 支持延迟加载
- 获取对象不需要锁,性能好
缺点:存在序列化和反序列化问题
6.静态内部类
它既满足懒汉式的懒加载,又满足饿汉式的高性能无锁,因为它也利用了类初始化机制,没有线程安全问题,不一样的是,它使用静态内部类,只要不去使用内部类,jvm就不会去加载它,也不会创建实例,这种代码很简洁。
public class Instance {
private static class InstanceHodler {
public static Instance instance = new Instance();
}
private Instance() {}
public static Instance getInstance() {
return InstanceHodler.instancel; //这里才初始化instance
}
}
静态内部类的优点:
- 线程安全
- 支持懒加载
- 不需要锁
缺点:存在序列化和反序列化问题
7.枚举
public enum Singleton {
INSTANCE; //该对象全局唯一
public static getInstance() {
reutn INSTANCE;
}
}
最完美的方法,Java枚举类本身的特性就保证了线程安全和唯一性,枚举类在序列化的时候会自动执行readResovle()方法,返回唯一实例,而不会创建对象,但是大量的枚举类会导致项目难以维护,难以理解。
总结
有没有发现除了枚举类的方法,其他方法都存在序列化和反序列的问题,是因为,对象在反序列和序列化时,会新创建一个对象并且返回,如果想解决这个问题,只要在类中重写readResovle方法就好了,return instance; 就ok了,每种方法各有优缺,如果都重写readResovle方法就会导致很繁琐,所以适合自己的项目就好,如果有疑问,可以评论区留言,共同探讨!