单例模式
创建型模式的主要关注点是“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。本文讲解创建型模式中的单例模式。
单例模式(Singleton Pattern)涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
# 1. 结构
单例模式的主要有以下角色:
- 单例类:只能创建一个实例的类
- 访问类:使用单例类
# 2. 实现
单例设计模式分为两类:
- 饿汉式:类加载就会导致该单实例对象被创建
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
# 2.1 饿汉式-方式 1:静态变量方式
/**
* 饿汉式 - 静态变量创建类的对象
*/
public class Singleton {
// 1. 私有构造方法
private Singleton() {}
// 2. 在成员位置创建该类的对象
private static Singleton instance = new Singleton();
// 3. 对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 构造方法私有从而使得外界无法访问构造方法,从而无法通过构造方法创建对象
- 存在的问题:instance 是随着类的加载而创建的,若对象很大却一直没有使用,则会造成内存浪费。
# 2.2 饿汉式-方式 2:静态代码块方式
/**
* 饿汉式 - 在静态代码块中创建该类对象
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 在成员位置创建该类的对象
private static Singleton instance;
static {
instance = new Singleton();
}
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 静态成员位置声明,静态代码块中创建
- 仍然是随着类的加载而创建,所以和方式 1 基本上一样
# 2.3 懒汉式-方式 1:线程不安全
/**
* 懒汉式 - 线程不安全
*/
public class Singleton {
// 私有构造方法
private Singleton() {}
// 在成员位置创建该类的对象
private static Singleton instance;
// 对外提供静态方法获取该对象
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 静态成员位置声明,在首次调用
getInstance()
时创建对象,从而实现懒加载的效果 - 但如果是多线程环境,会出现线程安全的问题。也就是可能两个线程同时都进入了
if(instance == null)
里面。
# 2.4 懒汉式-方式 2:线程安全
/**
* 懒汉式 - 线程安全
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 在
getInstance()
方法上加了一个 synchronized 同步锁,使得同一时刻只有一个线程能够执行该方法。 - 但这种方式效率很低,因为只在初始化 instance 时才有线程安全问题,一旦初始化完成就不存在了。
# 2.5 懒汉式-方式 3:双重检查锁
前一种方式存在的问题是,对于 getInstance
方法而言,绝大部分操作都是读操作,读操作是线程安全的,所以我们没有必要让每个线程必须持有锁才能调用该方法,因此我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式。
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
// 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null) {
synchronized (Singleton.class) {
// 抢到锁之后再次判断是否为null
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。但其实还存在问题,在多线程情况下可能会出现空指针的问题,出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile
关键字, volatile
关键字可以保证可见性和有序性。
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 注意 line 9 中 instance 声明时带上 volatile
- 注意 line 17 在抢到锁后再次判空
上面的代码就是双重检查锁的经典实现代码。
# 2.6 ⭐️ 懒汉式-方式 4:静态内部类方式
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被 static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
/**
* 静态内部类方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 第一次加载 Singleton 类时不会去初始化
INSTANCE
,只有第一次调用getInstance
,虚拟机才会加载 SingletonHolder 并初始化INSTANCE
,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
这种方式的单例模式是优秀的,在开源项目中常用。它在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
# 2.7 ⭐️ 枚举方式
枚举类实现单例模式是极力推荐的单例实现模式。因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
/**
* 枚举方式
*/
public enum Singleton {
INSTANCE;
}
2
3
4
5
6
- 这样每次
Singleton.INSTANCE
得到的都是同一个对象 - 属于饿汉式方式
# 3. 存在的问题
# 3.1 问题演示
有一种问题叫做破坏单例模式:使定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。
# 1)用序列化与反序列化来破坏
Singleton 类:
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Test 类:
ublic class Test {
public static void main(String[] args) throws Exception {
//从文件中读取对象从而反序列化
Singleton s1 = readObjectFromFile();
Singleton s2 = readObjectFromFile();
//判断两个反序列化后的对象是否是同一个对象
System.out.println(s1 == s2);
}
2
3
4
5
6
7
8
9
将一个 singleton 对象序列化到一个 file 中,之后再两次从该 file 反序列化回 object,会发现这两个 object 是两个对象,从而破坏了单例设计模式。
# 2)用反射来破坏
Singleton 类
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Test 类:
public class Test {
public static void main(String[] args) throws Exception {
//获取Singleton类的字节码对象
Class clazz = Singleton.class;
//获取Singleton类的私有无参构造方法对象
Constructor constructor = clazz.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
//创建Singleton类的对象s1
Singleton s1 = (Singleton) constructor.newInstance();
//创建Singleton类的对象s2
Singleton s2 = (Singleton) constructor.newInstance();
//判断通过反射创建的两个Singleton对象是否是同一个对象
System.out.println(s1 == s2);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 上面代码运行结果是
false
,表明序列化和反序列化已经破坏了单例设计模式
枚举方式不会出现这两个问题。
# 3.2 问题的解决
# 1)序列化与反序列化破坏单例的解决方法
在 Singleton 类中添加 readResolve()
方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 2)反射方式破解单例的解决方法
public class Singleton {
//私有构造方法
private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- 这种方式比较好理解。当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作
# 4. JDK 源码解析:Runtime 类
JDK 的 Runtime 类就是使用的单例设计模式。
我们通过源代码查看使用的是哪儿种单例模式:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
2
3
4
5
6
7
8
9
- 从上面源代码中可以看出 Runtime 类使用的是饿汉式(静态属性)方式来实现的单例模式。