单例模式

简介

单例模式,也叫单件模式。顾名思义,就是程序中只有一个实例。一般在我们实际工作中,会经常遇到这个模式,所以,掌握这个模式是应该的。

定义

单件方式可以有很多中方法定义,可根据不同的应用场景来选择最合适的定义方式。根据目前所学习的知识,可分为以下五种,某些可能在多线程的环境下回出现安全问题,需要改进。

《Head Filst 设计模式》中定义为:

单件模式确保程序中一个类只有一个实例,并且能够提供访问这个实例的全局访问点。

饥汉式

也叫饿汉式,二话不说 ,当jvm加载类时,就将类的静态实例初始化。

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

// 构造器私有化
private SingleInstanceT00() {

}

public static SingleInstanceT00 instance = new SingleInstanceT00();

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

在没有空间限制的情况下,可以选择这样使用。但是更优雅的做法是,调用的时候进行初始化,也就是延迟初始化。

延迟初始化

延迟初始化会带来线程安全的问题,比如改造后的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SingleInstanceT01 {

// 构造器私有化
private SingleInstanceT01() {

}

public static SingleInstanceT01 instance = null;

public static SingleInstanceT01 getInstance() {
if (instance == null) {
try {
// 模拟多线程,使效果更明显
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new SingleInstanceT01();
}
return instance;
}
}

经测试后发现,多个线程同时进入getInstance后,得到的不是同一个实例。

为了使得线程安全,解决方案也分为很多种,有synchronized同步方法、双重检查锁定和静态内部类初始化三种。

同步方法

使用synchronized对getInstance方法进行同步处理,但会导致不必要的性能开销。如果getInstance()方法被多个线程频繁调用,将会导致程序执行的性能下降。只有不频繁调用的时候,可能是理想状态。实现方式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SingleInstanceT02 {

// 构造器私有化
private SingleInstanceT02() {

}

public static SingleInstanceT02 instance = null;

// 同步方法作用域太大,性能低
public static synchronized SingleInstanceT02 getInstance() {
if (instance == null) {
try {
// 模拟多线程,使效果更明显
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new SingleInstanceT02();
}
return instance;
}
}

使用synchronized 同步方法时可以不用加volatile,因为synchronized 可以保证原子性。

双重检查锁定

双重检查锁定使用synchronized同步代码块的方式成功的优化了性能。只有第一次进来的时候,才需要加锁。其他时候进来的时候先判断instance是否为空,如果不为空的话,就直接进行返回。

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 SingleInstanceT03 {

// 构造器私有化
private SingleInstanceT03() {

}

public static volatile SingleInstanceT03 instance = null;

public static SingleInstanceT03 getInstance() {
if (instance == null) {
try {
// 模拟多线程,使效果更明显
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}

// 同步代码块 DCL
synchronized (SingleInstanceT03.class) {
if (instance == null) {
instance = new SingleInstanceT03();
}
}
}
return instance;
}
}

注意:加volatile是为了防止指令的重排序,在jvm执行指令时,instance = new SingleInstanceT03() 分了三步:

第一步:给对象分配内存空间

第二步:成员变量初始化

第三步:将instance指向刚分配的内存地址(此时还是半初始化状态,有属性的话,属性尚未赋值)

如果没有加volatile,第二步和第三步会交换顺序执行,导致其他线程拿到的是半初始化的实例,拿到未初始化的数据(比如秒杀系统拿到的值为0,带来重大安全隐患)。

类初始化

这也是延迟初始化的一种,基于类初始化实现的。实现原理是每个对象对应有一个初始化锁,初始化时线程需要获取该对象对应的初始化锁。如果没有获取,那么线程必须等到初始化锁释放了才能获取。

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

private static class InnerInitialClass {
public static SingleInstanceT04 instance = new SingleInstanceT04();
}

// 原理:每个对象对应有一个初始化锁,初始化时线程需要获取该对象对应的初始化锁
public static SingleInstanceT04 getInstance() {
try {
// 模拟多线程,使效果更明显
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return InnerInitialClass.instance;
}
}

应用

在《Head Filst 设计模式》提到,使用单件模式的地方很多。常见的有线程池、连接池、缓存、日志对象。

还用一些不常见的,对话框、处理偏好设置、注册表的对象、充当打印机、显卡等设备的驱动程序的对象。