# 单例模式
本文作者:程序员飞云
# 单例模式(Singleton)
保证一个类只有一个实例,并且能够提供一个访问它的全局访问点。避免创建多次实例,节省内存空间。
# 1. 懒汉模式----线程不安全
先看一下最简单代码
public class Singleton {
private static Singleton singleton;
// 防止外界手动new实例化的可能
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
既然是单例模式,只有一个实例,可以编写下相应的测试代码正确
public class Main {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2); // 输出 true
}
}
2
3
4
5
6
7
运行结果true也是认为是同一个实例。
仔细思考下上面代码如果在多线程情况下会出现什么问题,很明显,线程之前会抢占资源,多线线程同时访问这个实例,实例此时都是null,都能过if条件,会创建多个实例,那就违背了单例的特性。
# 2. 懒汉式----单锁校验,线程安全
这个方式最简单,只需要在方法上面加上锁就可以
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
2
3
4
5
6
能够保证只有一个线程能够进入方法执行,只会产生一个实例,但是每次调用getInstance都需要先获取锁,然后再释放锁,无疑是会浪费一定的性能。这里锁加在方法上,而不是实例,最根本的原因是目前线程不知道有没有创建过实例。
# 3. 双锁校验—–线程安全
上面的突破口在于其他线程怎么知道是否创建过实例,这个可以通过一个关键字volatile
保证。
volatile
关键字能够保证多线程之间变量的可见性,这个可以看一下JavaGuide (opens new window)的一张图
主要就是每次线程对数据进行修改后,它会将这个修改的记录同步至主内存,然后主内存也修改,其他的线程也就能够获取到最新的数据。
详细代码如下。
public class Singleton {
// 多线程之间变量可见
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
// 判断实例是否为空
if (singleton == null) {
// 锁住当前实例
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 第一个if是用于判断实例是否存在,只有实例为空的时候才能进入下一步
- synchronized是防止多个线程同时进入创建实例
- 第二个if也是用于判断实例是否存在,但是区别在于多线程的环境下,可能有多个线程同时调用
getInstance
,而他们的实例都是null,很容易突破第一个if判断,线程依次等待进入锁创建实例,第一个线程创建完实例,第二个线程也要进入锁来创建实例,很明显会出现问题
这样也就大大减少了获取锁以及释放锁的时间,提升了性能。
为什么又叫懒汉式,因为它只有在第一次被引用的时候,才会将自己实例化
# 4. 饿汉式单例----静态初始化,线程安全
上面这个方式依然会有一点占用性能,所以还有一个更好的方法
public class Singleton {
private static Singleton singleton = new Singleton();
// 私有构造函数,防止外部直接创建实例
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
2
3
4
5
6
7
8
9
10
因为构造方法是私有的,除了类本身其他类无法实例化Singleton,所以可以在自己被加载的时候就实例化,需要提前占用部分系统资源,所以称为饿汉式。
# 5. 静态内部类----线程安全
在类加载的过程中,静态内部类不会被加载,只有在调用 getInstance
方法时,Holder
类才会被加载,从而实例化 Singleton
。
这种方式的优点是简单且线程安全,而且不需要使用 synchronized
关键字。
public class Singleton {
// 私有构造函数,防止外部直接创建实例
private Singleton() {
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
private static class Holder {
// 在静态内部类中初始化单例实例
private static final Singleton INSTANCE = new Singleton();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6. 枚举单例模式----线程安全
这个好像是目前最佳的方法,反射安全,序列化安全,实现简单等等,因为前面几个方式都是使用到了构造方法,而我们知道通过反射就能动态的创建类,获取类的内部信息,操作类等等,所以相对而言构造方法并不是台安全的。反射在通过newInstance创建对象时,会检查该类是否ENUM修饰。
具体可以查看这篇博客https://cloud.tencent.com/developer/article/1497592 (opens new window)
public enum Singleton {
INSTANCE;
public void method() {
System.out.println("我是一个单例!");
}
}
2
3
4
5
6
public static void main(String[] args) {
Singleton singleton1 = Singleton.INSTANCE;
Singleton singleton2 = Singleton.INSTANCE;
System.out.println(singleton1 == singleton2); // 输出 true
}
2
3
4
5
# 反射测试
先测试一下反射,来测试下是否可行,是否真的有效果
# 静态初始化
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
// 使用空构造函数new一个实例。即使它是private的
Singleton rSingleton = constructor.newInstance();
System.out.println(singleton);
System.out.println(rSingleton);
System.out.println(singleton == rSingleton); // false
}
2
3
4
5
6
7
8
9
10
11
12
很显然,出现了不是同一个实例的问题,静态初始化没办法防御反射
# 枚举单例
public class Main {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Singleton singleton = Singleton.INSTANCE;
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
// 使用空构造函数new一个实例。即使它是private的
Singleton rSingleton = constructor.newInstance();
System.out.println(singleton);
System.out.println(rSingleton);
System.out.println(singleton == rSingleton); // false
}
}
2
3
4
5
6
7
8
9
10
11
12
13
运行报错,我这边也是翻了下enum的源码,enum是有一个构造方法
那么可以修改下上面的测试代码,添加相应的构造类型
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class,int.class);
然后依然是报错
根据报错信息不能创建一个enum的反射,然后查看了对应的错误位置,发现了以下代码
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
2
3
只要检测到是枚举值的时候就会报错,所以是能够防御反射的。
# 序列化测试
# 静态初始化
需要实现Serializable接口
Spring里面自带了一个SerializationUtils实现序列化与反序列化的工具
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton);
// 进行序列化和反序列化操作
byte[] bytes = SerializationUtils.serialize(singleton);
Object deserialize = SerializationUtils.deserialize(bytes);
System.out.println(deserialize);
System.out.println(deserialize == singleton);
}
2
3
4
5
6
7
8
9
10
结果很明显,不是同一个实例了
# 枚举单例
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
System.out.println(singleton);
// 进行序列化和反序列化操作
byte[] bytes = SerializationUtils.serialize(singleton);
Object deserialize = SerializationUtils.deserialize(bytes);
System.out.println(deserialize);
System.out.println(deserialize == singleton);
}
2
3
4
5
6
7
8
9
10
运行结果显示都是同一个单例所以,也是安全的
# 单例模式的优点
- 节省系统资源:只创建了一个实例
- 简化了对象访问:只提供了一个获取实例的方法
- 饿汉式单例模式:类加载的时候创建单例对象,缺点是不支持延迟加载
- 懒汉式单例模式:只有第一次使用的时候会创建实例,缺点是多线程安全
- 双锁校验单例模式:在第一次使用时创建实例,缺点是仍然有一点性能问题
- 枚举单例模式:使用简单,能够防御反射,序列化的问题,推荐使用。
# 使用场景
- 数据库连接池不会反复创建
- 配置文件管理器
- Spring里面的ApplicationContext提供了一个全局访问点,保证只有一个bean
- 个人项目:飞云代码生成器里面就使用到了单例模式,在读取json形式的meta数据的时候,只需要读取一次就可以,然后其他的里面字段信息通过提供的一个接口进行获取,有效减少了资源的消耗。