深入理解单例模式

  1. 为什么静态内部类的单例模式是最推荐的?
  2. 如何在反射的情况下保证单例
  3. 如何在反序列化中保证单例

为了回答上面三个问题,下面将逐步进行分析

1. 饿汉式——线程安全的单例模式
1
2
3
4
5
6
7
8
9
public class Singleton1 {
private static Singleton1 instance = new Singleton1();

private Singleton1(){}

public static Singleto1 getInstnance() {
return instance;
}
}

优点:线程安全;缺点:类加载的时候已经实例化,浪费空间

2. 懒汉式
(1)懒汉式V1
1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazySingleton1() {
private static LazySingleton1 instance;

private LazySingleton1(){}

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

return instance;
}
}

线程不安全,给方法加锁

1
2
3
4
5
6
public static synchronized LazySingleton1 getInstance() {
if (instance == null) {
instance = new LazySingleton1();
}
return instance;
}

由于将synchronized关键字加在方法上会造成线程阻塞(同步),在性能上会大打折扣。所以使用双重校验锁(在保证线程安全的同时提高了性能。)

1
2
3
4
5
6
7
8
9
10
public static LazySingleton1 getInstance() {
if (instance == null) {
synchronized (LazySingleton1.class) {
if (instance == null) {
instance = new LazySingleton1();
}
}
}
return instance;
}
(2) 懒汉式V2

内部类加载机制

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
public class OuterTest {

static {
System.out.println("load outer class...");
}

static class StaticInnerTest {
static {
System.out.println("load static inner class...");
}

static void staticInnerMethod() {
System.out.println("static inner method...");
}
}

public static void main(String[] args) {
OuterTest outerTest = new OuterTest();
System.out.println("===========//=========");
OuterTest.StaticInnerTest.staticInnerMethod();
}
}

输出:
load outer class...
===========//=========
load static inner class...
static inner method...

说明:1. 加在一个类时,其内部类不会被同时加载。2. 一个类被加载,当且仅当某个静态成员(静态域,构造器,静态方法等)被调用时发生。


 回到第一版的双重校验锁。其实不管性能如何优越,还是使用了synchronized关键字,那么对性能始终是有影响的(有兴趣的话可以了解一下synchronized的内存模型与底层原理)。所以,下面给出了改良版本——使用静态内部类的方式

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
public class LazySingleton2 {
private LazySingleton2() {
}

static class SingletonHolder {
private static final lazySingleton2 instance = new LazySingleton2();
}

public static LazySingleton2 getInstance() {
return SingletonHolder.instance;
}
}

class MyThread extends Thread{
@Override
public void run() {
System.out.println(LazySingleton2.getInstance().hashCode());
}
}

class Run{
public static void main(String[] args) {

MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();

t1.start();
t2.start();
t3.start();
}
}

结果:
1385166630
1385166630
1385166630

 由于对象实例化是在内部类加载的时候构建的,因此是线程安全的(在方法中创建对象才存在并发问题,静态内部类随着方法的调用而被加载,只加载一次,不存在线程安全问题)。

 在getInstance()方法中没有使用synchronized关键字,因此没有造成多余的性能损耗。当LazySingleton2类加载的时候,其静态内部类SingletonHolder并没有被加载,因此instance对象没有被构建。

 而我们在调用LazySingleton2.getInstance()方法时,内部类SingletonHolder被加载,此时单例对象才被构建。因此,这种写法节约空间,达到了懒加载的目的。

(3) 懒汉式V3

使用静态内部类的懒加载方式虽然具有很多优良特性,但是在反射的作用下,单例结构还是会被破坏:

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
public class LazySingleton2Test {

public static void main(String[] args) {
//创建第一个实例
LazySingleton2 instance1 = LazySingleton2.getInstance();

//通过反射创建第二个实例
LazySingleton2 instance2 = null;

try {
Class<LazySingleton2> clazz = LazySingleton2.class;
Constructor<LazySingleton2> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
instance2 = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
//检验两个实例的hashcode值
System.out.println("Instance1's hashcode:" + instance1.hashCode());
System.out.println("Instance2's hashcode:" + instance2.hashCode());
}
}

结果:
Instance1's hashcode:21685669
Instance2's hashcode:2133927002

由于上面的问题,所以诞生了第三版的懒汉式:

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
public class LazySingleton3 {

private static boolean initialized = false;

private LazySingleton3() {
synchronized (LazySingleton3.class) {
if (initialized == false) {
initialized = !initialized;
} else {
throw new RuntimeException("单例已经被破坏!");
}
}
}

static class SingletonHolder {
private static final LazySingleton3 instance = new LazySingleton3();
}

public static LazySingleton3 getInstance() {
return SingletonHolder.instance;
}
}

经过测试后输出为:
java.lang.reflect.InvocationTargetException
trueat sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
trueat sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
trueat sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
trueat java.lang.reflect.Constructor.newInstance(Constructor.java:423)
trueat com.yuangh.concurrent4.demo3.LazySingleton2Test.main(LazySingleton2Test.java:22)
Caused by: java.lang.RuntimeException: 单例已经被破坏!
trueat com.yuangh.concurrent4.demo3.LazySingleton3.<init>(LazySingleton3.java:16)
true... 5 more
Exception in thread "main" java.lang.NullPointerException
trueat com.yuangh.concurrent4.demo3.LazySingleton2Test.main(LazySingleton2Test.java:28)
Instance1's hashcode:1836019240

这就保证了反射无法破坏其单例性。

(4) 懒汉式V4

在分布式系统中,有些情况下需要在单例中实现Serializable接口。这样就可以在文件系统中存储它的状态并且在稍后的某一时间点取出。

将 public class LazySingleton3 修改为 public class LazySingleton3 implements Serializable

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
public class LazySingleton3Test {

public static void main(String[] args) {
try {
//将对象序列化到文件
LazySingleton3 instance1 = LazySingleton3.getInstance();
ObjectOutput out = null;
out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
out.writeObject(instance1);
out.close();

//从文件反序列化到对象
ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
LazySingleton3 instance2 = (LazySingleton3) in.readObject();
in.close();
System.out.println("instance1's hashcode:" + instance1.hashCode());
System.out.println("instance2's hashcode:" + instance2.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}

}

得到结果为:
instance1's hashcode:856419764
instance2's hashcode:1480010240

 显然,出现了两个实例类,说明破坏了单例模式。我们需要提供 readResolve() 方法的实现。readResolve()代替了从流中读取对象。这就确保了在序列化和反序列化的过程中没人可以创建新的实例。下面是懒汉式第四个版本:

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
public class LazySingleton4 implements Serializable {

private static boolean initialized = false;

private LazySingleton4() {
synchronized (LazySingleton3.class) {
if (initialized == false) {
initialized = !initialized;
} else {
throw new RuntimeException("单例已经被破坏!");
}
}
}

static class SingletonHolder {
private static final LazySingleton4 instance = new LazySingleton4();
}

public static LazySingleton4 getInstance() {
return SingletonHolder.instance;
}

private Object readResolve() {
return getInstance();
}
}

测试后输出结果为:
instance1's hashcode:856419764
instance2's hashcode:856419764
总结

 在项目开发中根据情况选择相应的版本使用即可,并没有强制要求使用哪个版本。当然,一般来说,第二个版本(懒汉式V2)是使用的比较多的。

-------------本文结束感谢您的阅读-------------
老铁,打赏一点儿呗