设计模式之单例模式
单例模式(Singleton Pattern)是一个比较简单的模式,定义如下:
Ensure a class has only one instance, and provide a global point of access to it.
确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
以下是单例模式的几种写法:
懒汉式–线程不安全
1 | public class Singleton{ |
代码简洁明了,使用了懒加载模式,但是在多个线程并行调用getInstance()的时候,会创建多个实例,在多线程下不能正常工作。
懒汉式–线程安全
为了解决上述问题,最简单的方法就是将整个getInstance()方法设为同步(synchronized).
1 | public static synchronized Singleton getInstance(){ |
这种方式做到了线程安全,但是并不高效,任何时候都只能有一个线程调用getInstance()方法,同步操作只有第一次调用时才需要。于是有人引出了双重检验锁。
双重检验锁
双重检验锁(double checked locking pattern)是一种使用同步块加锁的方法。我们称其为双重检验锁,因为会有两次检查instance == null
,一次是在同步块外,一次是在同步块内。
为什么要检查两次,因为可能会有多个线程一起进入同步块的if,如果不进行二次检验就会生成多个实例
1 | public static Singleton getSingleton(){ |
这段代码看上去完美,但是还是有问题,主要在于instance = new Singleton()并非是一个原子操作,事实上在JVM中这句话大概做了3件事情:
- 给instance分配内存
- 调用Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间
但是在JVM的即时编译器中存在指令重排序的优化,即上述第二步和第三步的顺序是不能保证的,最终执行顺序可能是1-2-3也可能是1-3-2。如果是后者,则在3执行完毕,2未执行之前被线程二抢占,这时instance 已经是非null(没有初始化),线程2会直接返回instance,然后使用时报错。
我们只需要将instance变量声明为volatile
1 | public class Singleton{ |
volatile关键字的作用:
- 保证变量在内存中的可见性
- 保证程序执行的顺序,volatile关键字禁止指令重排
这里其实用到的是volatile关键字的有序性,确保JVM执行时不会进行指令重排。
饿汉式–static final field
直接将单例的实例声明成static和final变量,这样在类第一次加载时就在内存中初始化,所以实例本身是线程安全的。
1 | public class Singleton{ |
这种写法的缺点在于单例会在加载类一开始就被初始化,这导致了饿汉式在某些场景中无法使用,比如Singleton实例的创建依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,这样这种单例写法无效了。
静态内部类 static nested class
这种方法是《effective Java》上所推荐的。
1 | public class Singleton{ |
这种写法使用JVM本身的机制来保证了线程安全的问题,由于SingletonHolder是私有的,除了getInstance()方法没有办法访问到他,同时读取实例的时候不会进行同步,没有性能缺陷,不依赖于JDK版本。
枚举 Enum
用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。
1 | public enum EasySingleton{ |
我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。
几种创建方式的总结
一般来说,单例模式有五种线程安全的写法:懒汉,饿汉,双重检验锁,静态内部类,枚举。
单例模式的使用场景
在以下场景适合单例模式:
- 有频繁实例化然后销毁的情况。
- 创建对象耗时多或者消耗资源多而又使用频繁。
- 频繁访问IO资源的对象,例如数据库连接池或者访问本地文件。
比如说:
网站在线人数统计
这其实就是一个全局计数器,所有用户在同一时刻获取的在线人数数量都是一致的,这里包含分布式场景。下面代码简单实现一个计数器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class Counter{
private static class CounterHolder{
private static final Counter counter = new Counter();
}
private Counter(){
System.out.println("init...");
}
private static final Counter getInstance(){
return CounterHolder.counter;
}
private AtomicLong online = new AtomicLong();
public long getOnline(){
return online.get();
}
public long add(){
return online.increasementAndGet();
}
}
配置文件访问类
项目种经常需要一些环境相关的配置文件,比如短信通知相关,邮件相关。比如使用Spring,可以使用@PropertySource注解实现,默认就是单例模式,不用单例的话,每次都要new对象,读取配置文件。
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
36public class SingleProperty{
private static Properties prop;
private static class SinglePropertyHolder{
private static final SingleProperty singleProperty = new SingleProperty();
}
/**
* config.properties 内容是 test.name=kite
*/
private SingleProperty(){
System.out.println("构造函数执行");
prop = new Properties();
InputStream stream = SingleProperty.class.getClassLoader()
.getResourceAsStream("config.properties");
try {
prop.load(new InputStreamReader(stream, "utf-8"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static SingleProperty getInstance(){
return SinglePropertyHolder.singleProperty;
}
public String getName(){
return prop.get("test.name").toString();
}
public static void main(String[] args){
SingleProperty singleProperty = SingleProperty.getInstance();
System.out.println(singleProperty.getName());
}
}数据库连接池的实现
做池化的原因就是新建连接十分耗时,一般做法是在应用内维护一个连接池,这样当任务进来时,如果有空闲连接可以直接拿来用,省去了初始化的开销。所以单例模式正好实现一个应用内只有一个线程池,所有的连接任务都需要从连接池里获取连接。