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

什么是单例

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

单例类具备哪些要求

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

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

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

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

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!
此时singleton为null,未被初始化!”

满足延迟加载的要求。

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

使用枚举类实现单例

1
2
3
public enum TantanitLogoEnum {
singleton
}

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

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

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

© 2022 谈谈IT All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero