Effective Java 第二版笔记之使用私有构造器或枚举实现单例

字数1,455 大约花费6分钟

目录

  1. 1. 什么是单例
  2. 2. 单例类具备哪些要求
  3. 3. 访问权限控制
  4. 4. 解决反射攻击问题(无除枚举外的其它方式)
  5. 5. 解决反序列化问题
  6. 6. 线程安全
  7. 7. 延迟加载
  8. 8. 使用枚举类实现单例

什么是单例

单例是指只会初始化一次,因而最多只会有一个实例的类。单例一般用来表示本质上只有一个的组件。比如操作系统中的窗体管理器和文件系统等。

单例类具备哪些要求

在使用单例时,需要考虑以下几点:

  • 访问权限控制,应当使用私有属性或方法生成实例
  • 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)
  • 反序列化问题(防止多次反序列化生成多个不同的实例)
  • 线程安全(防止不同线程生成多个不同的实例)
  • 是否使用延迟加载,只在需要的时候才生成实例

如果不考虑延迟加载的问题,枚举是实现单例的最佳选择。

下面以一个完整的例子讲解在不使用枚举的情况下,做到以上几点,(除了反射攻击)。

1
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
31
32
33
34
35
36
37
38
39
40
public class TantanitLogo implements Serializable {
private volatile static TantanitLogo singleton;
private volatile static boolean initialized = false;

private TantanitLogo() {
synchronized (TantanitLogo.class) {
if (initialized == false) {
initialized = true;
} else {
throw new RuntimeException("受到反射攻击!");
}
}

}

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

}

private Object readResolve() {
return singleton;
}

public static void otherMethod() {
System.out.println("执行 TantanitLogo 类的静态方法 otherMethod!");
if (TantanitLogo.singleton == null) {
System.out.println("此时 singleton 为 null,未被初始化!");
} else {
System.out.println("此时 singleton 不为 null,已被初始化!");
}
}
}

访问权限控制

例子中成员变量 singleton 和构造器都是私有类型的,实现了访问权限控制。

解决反射攻击问题(无除枚举外的其它方式)

目前除了使用枚举似乎没有其它方法可以解决反射攻击,以下代码仍然无法避免反射攻击。

1
2
3
4
5
6
7
8
9
10
11
12
private volatile static boolean initialized = false;

private TantanitLogo() {
synchronized (TantanitLogo.class) {
if (initialized == false) {
initialized = true;
} else {
throw new RuntimeException("受到反射攻击!");
}
}

}

这段代码本意是,添加成员变量 initialized,用来标识是否生成过实例,在调用构造函数时,如果已经调用过一次,生成过实例,则报错。但如果使用反射的方式先将 initialized 改为 false,再调用私有构造函数,就可以顺利绕过 initialized,生成第二个实例,破坏单例性。

解决反序列化问题

TantanitLogo 类实现了 Serializable 接口,可以被序列化和反序列化,为类添加的 readResolve 方法,可以解决反序列化时生成新的实例的问题。

1
2
3
private Object readResolve() {
return singleton;
}

在 TantanitLogoTest 类添加以下测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 测试反序列化
*/

public static void testDeserialize() throws IOException, ClassNotFoundException {
TantanitLogo tantanitLogo1 = TantanitLogo.getInstance();


FileOutputStream fos = new FileOutputStream("object.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(tantanitLogo1);
oos.close();
fos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.out"));
TantanitLogo tantanitLogo3 = (TantanitLogo) ois.readObject();
if (tantanitLogo1 == tantanitLogo3) {
System.out.println("tantanitLogo1 与 tantanitLogo3 是同一个实例");
} else {
System.out.println("tantanitLogo1 与 tantanitLogo3 不是同一个实例");
}
}

当 TantanitLogo 类中有 readResolve 方法时,ObjectInputStream 的 readObject 方法会调用 readResolve 方法,所以输出结果为“tantanitLogo1 与 tantanitLogo3 是同一个实例”,当 TantanitLogo 类中没有 readResolve 方法时,则输出“tantanitLogo1 与 tantanitLogo3 不是同一个实例”。

线程安全

1
2
3
4
5
6
7
8
9
10
11
public static TantanitLogo getInstance() {
if (singleton == null) {
synchronized (TantanitLogo.class) {
if (singleton == null) {
singleton = new TantanitLogo();
}
}
}
return singleton;

}

上一篇文章 中讲解了使用静态工厂方法代替构造器的好处,这里就是使用 getInstance 方法就是代替构造器,生成实例。而使用 synchronized 关键字达到线程安全的目的,您可能注意到,我在 synchronized 代码块外加了 singleton == null 的条件判断。这是由于只有当 singleton 为 null 时才会进行 new 操作,生成新的实例,所以只在这个时候对代码加同步限制。

延迟加载

以下是 TantanitLogo 中的另一个静态类方法:

1
2
3
4
5
6
7
8
public static void otherMethod() {
System.out.println("执行 TantanitLogo 类的静态方法 otherMethod!");
if (TantanitLogo.singleton == null) {
System.out.println("此时 singleton 为 null,未被初始化!");
} else {
System.out.println("此时 singleton 不为 null,已被初始化!");
}
}

由于只在 TantanitLogo 的静态方法 getInstance 中进行 new 操作,生成新的实例。所以调用其它静态方法不会生成新的实例。在 TantanitLogoTest 添加方法进行测试:

1
2
3
4
5
6
/**
* 测试延迟加载
*/

public static void testLazyLoad() {
TantanitLogo.otherMethod();
}

输出结果为

1
2
执行 TantanitLogo 类的静态方法 otherMethod!
此时 singletonnull,未被初始化!”

满足延迟加载的要求。

您可以以每次启动调用一个测试函数的方法,对以上几个特征分别进行测试。如果您觉得哪个特征在您的应用场景中不重要,也可以很容易地进行简化。

使用枚举类实现单例

1
2
3
public enum TantanitLogoEnum {
singleton
}

使用只有一个元素的枚举类可以很方便地实现单例,并且满足除了延迟加载之外的所有要求:

  • 反射攻击(防止通过反射调用私有属性或方法,生成新的实例)
  • 反序列化问题(防止多次反序列化生成多个不同的实例)
  • 线程安全(防止不同线程生成多个不同的实例)

下篇文章,将会对 Enum 类的工作原理进行解析,并解释为什么枚举具有这些优势。

谈谈 IT的文章均为原创或翻译(翻译会注明外文来源),转载请以链接形式标明本文地址: http://tantanit.com/effectiv-java-use-how-to-create-a-singleton/

谈谈IT

欢迎关注官方微信公众号获取最新原创文章