Java设计模式系列(1):单例模式

布鸽不鸽 Lv4

前言

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种模式涉及单一的类,需确保代码中最多只有一个该类对象被创建。

原文地址:https://xuedongyun.cn/post/25714/

懒汉式

饿汉式,在一开始就创建对象

方法一:静态成员变量

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
// 私有化构造器
private Singleton() {}

// 成员变量位置创建对象
private static Singleton instance = new Singleton();

// 静态方法提供对象
public static Singleton getInstance() {
return instance;
}
}

方法二:静态代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private Singleton() {}

private static Singleton instance;

// 使用静态代码块
static {
instance = new Singleton();
}

public static Singleton getInstance() {
return instance;
}
}

饿汉式

方式一:线程不安全

第一次使用时才创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
// 线程不安全
public class Singleton {
private Singleton() {}

private static Singleton instance;

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

方式二:线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
// 线程安全,使用synchronized关键词
public class Singleton {
private Singleton() {}

private static Singleton instance;

public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

方式三:双重检查锁

因为绝大多数操作都是读操作,给方法加锁影响性能,可以改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
private Singleton() {}

private static Singleton instance;

public static Singleton getInstance() {
if (instance == null) {
// 写操作时,给类加锁
synchronized (Singleton.class) {
// 抢到锁的时候还要判断一下是否已经被竞争者赋过值了
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

上述代码看起来很完美,但实际上可能会有空指针问题,原因是JVM在实例化对象的时候会进行指令重排

在java中,创建对象实际上不是不是原子操作,而是有三步:

1
2
3
4
5
6
7
8
// 1. 分配对象的内存空间
memory = allocate();

// 2. 初始化对象
ctorInstance(memory);

// 3. 设置instance指向刚分配的内存地址
instance = memory;

指令重排后,就可能变成:

1
2
3
4
5
6
7
8
// 1. 分配对象的内存空间
memory = allocate();

// 2. 设置instance指向刚分配的内存地址
instance = memory;

// 3. 初始化对象
ctorInstance(memory);

上述懒汉式代码中,很有可能:某个线程获取了非空instance,但是此时还没来得及初始化。我们可以使用volatile关键词来解决这个问题

volatile 关键字有如下特征:

  • 可见性:当一个线程修改了这个变量的值,volatile保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存
  • 禁止指令重排序优化:被volatile 修饰的变量,不会被JVM指令重排
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {

private Singleton() {}

// 使用volatile关键词解决指令重排的问题
private static volatile Singleton instance;

public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

方式四:静态内部类

首次使用时,静态内部类才会被加载,进而初始化INSTANCE。这是开源项目中一种较为常见的单例模式

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private Singleton() {}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

方法五:枚举方式

这是一种极力推荐的单例实现模式,因为枚举类是线程安全的,且只会装载一次。枚举类无法使用反射创建,因此是唯一不会被破坏的单例模式。

1
2
3
4
5
6
public enum Singleton {
INSTANCE;
private int value;
private String name;
// ...
}

存在的问题

序列化和反射能破坏单例模式(枚举方法除外)

序列化破坏单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Singleton instance = Singleton.getInstance();

// 序列化后存入文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:/test.txt"));
oos.writeObject(instance);

// 从文件中读取
ObjectInputStream ois1 = new ObjectInputStream(new FileInputStream("C:/test.txt"));
Singleton singleton1 = (Singleton) ois1.readObject();

// 从文件中读取
ObjectInputStream ois2 = new ObjectInputStream(new FileInputStream("C:/test.txt"));
Singleton singleton2 = (Singleton) ois2.readObject();

// singleton1和singleton2是两个对象,单例模式被破坏

反射破坏单例模式

1
2
3
4
5
6
Class<Singleton> clazz = Singleton.class;
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);

Singleton singleton1 = constructor.newInstance();
Singleton singleton2 = constructor.newInstance();

解决方法

解决序列化破坏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

原理:可以看看ObjectInputStream.readObject的源码:

readObject->readObject0->readOrdinaryObject->如果有readResolve()方法,在反序列化的时候会调用该方法返回对象

1
2
3
4
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
Object rep = desc.invokeReadResolve(obj);
// ...
}

解决反射破坏

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {

//私有构造方法
private Singleton() {

// 被反射调用时,直接抛出错误
if(instance != null) {
throw new RuntimeException();
}
}

// ...
}

JDK源码-Runtime类

Runtime类使用饿汉式(静态变量)实现

1
2
3
4
5
6
7
public class Runtime {
private static final Runtime currentRuntime = new Runtime();

public static Runtime getRuntime() {
return currentRuntime;
}
}

Runtime类的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Runtime runtime = Runtime.getRuntime();

// 虚拟机内存总量
long totalMemory = runtime.totalMemory();
System.out.println("totalMemory = " + totalMemory);

// 虚拟机试图使用的最大内存量
long maxMemory = runtime.maxMemory();
System.out.println("maxMemory = " + maxMemory);

// 创建新的进程,执行指定的字符串命令,返回进程对象
Process process = runtime.exec("ipconfig");

// 通过输入流,获取命令执行后的结果
InputStream is = process.getInputStream();
byte[] bytes = new byte[1024 * 1024 * 100];
int i = is.read(bytes);
System.out.println(new String(bytes, 0, i, "gbk"));
  • 标题: Java设计模式系列(1):单例模式
  • 作者: 布鸽不鸽
  • 创建于 : 2023-06-21 16:48:48
  • 更新于 : 2023-06-23 22:01:36
  • 链接: https://xuedongyun.cn//post/25714/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论