单例模式的实现方式

饿汉式

1
2
3
4
5
6
7
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}

注意:必须是私有构造方法,防止不会被其他代码实例化。这种实现唯一的不足是不能实现延迟加载。

懒汉式-不同步

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
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
14
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

注意:这种实现考虑了多线程的情况,但是由于做了同步,会导致性能较差(相对于饿汉式)。

懒汉式改造

1
2
3
4
5
6
7
8
9
public class StaticSingleton {
private StaticSingleton() {}
private static class SingletonHolder {
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
}

在这种实现中,使用内部类来维护单例的实例。当StaticSingleton被实例化时,其内部类并不会实例化。当getInstance()方法被调用时,才会加载SingletonHolder,从而初始化instance。
同时,由于实例的建立,是在类加载时完成的,所以天生对多线程友好。getInstance()方法也不需要同步关键字。
因此,这种实现既做到了延迟加载,又不用使用同步关键字。

使用枚举

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
/**
* @author j.tommy
* @version 1.0
* @date 2018/8/7
*/
public class SingleObject {

private SingleObject() {

}

private enum SingletonInstance {
INSTANCE;
private SingleObject instance;

//JVM会保证此方法绝对只调用一次
SingletonInstance() {
instance = new SingleObject();
}

public SingleObject getInstance() {
return instance;
}
}

public static SingleObject getInstance() {
return SingletonInstance.INSTANCE.getInstance();
}

public static void main(String[] args) {
// 测试100个线程获取单例实例对象是否是同一个。
IntStream.rangeClosed(1,100).forEach(i -> new Thread("t-"+i){
public void run() {
System.out.println(Thread.currentThread().getName() + "==>" + SingleObject.getInstance());
}
}.start());
}
}

确保反序列化仍然得到单例对象

通常,使用上面的方式创建的单例已经能确保是唯一的实例。但仍然有例外情况生成多个实例。比如,通过反射机制,强行调用类的私有构造方法。或者对象序列化/反序列化。
实现序列化接口的单例类:

1
2
3
4
5
6
7
public class SerSingleton implements Serializable {
private static SerSingleton instance = new SerSingleton();
private SerSingleton() {}
public static SerSingleton getInstance() {
return instance;
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SerSingleton s1 = SerSingleton.getInstance();
// 将单例对象串行化到文件
String filepath = "d:/SerSingleton.txt";
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filepath));
oos.writeObject(s1);
oos.flush();
oos.close();
System.out.println(s1);
// 从文件读出原有的单例对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filepath));
SerSingleton s2 = (SerSingleton) ois.readObject();
ois.close();
System.out.println(s2);
System.out.println(s1 == s2);

测试结果:

可以看到,经过反序列化后产生了不同的实例。

我们现在对SerSinglton增加一个readResolve()方法:

1
2
3
4
5
6
7
8
9
10
public class SerSingleton implements Serializable {
private static SerSingleton instance = new SerSingleton();
private SerSingleton() {}
public static SerSingleton getInstance() {
return instance;
}
private Object readResolve() { // 阻止生成新的实例,总是返回当前对象。
return instance;
}
}

再次测试:

可以看到,经过反序列化后得到的仍然是同一个实例对象。
事实上,在实现了私有的readResolve()方法后,readObject已经形同虚设,它直接使用了readResolve()替换了原本的返回值,从而在形式上构造了单例。

实际使用建议使用静态内部类或枚举的方式,既能保证延迟初始化,又是线程安全的。

参考:《Java程序性能优化-让你的Java程序更快、更稳定》(葛一宁等编著)》

Donny wechat
欢迎关注我的个人公众号
打赏,是超越赞的一种表达。
Show comments from Gitment