什么是单例模式
单例,就是单个对象单个实例的意思,是指在系统运行期间,某个对象类型最多只会创建出一个实例,创建和管理单例对象的方法就是单例模式。
选择使用单例对象的原因,通常都是因为创建这种对象的成本比较高,要么是很费时间,要么是很费资源。所以,就创建一个这样的对象,反复使用它就好了,能节约很多时间和资源。
通常,单例对象的生命周期都会比一般对象要长,一旦创建好了就常驻内存,随用随取。
单例对象可以有作用域的概念,比如,限定一个进程内只能有一个单例,或者一个线程内只能有一个单例,也可以将作用域和HTTP
的生命周期绑定,限定在一个完整的HTTP
请求和响应过程内只能有一个单例。还可以有其它作用域定义,当然,这需要额外的手段才能实现。
使用单例模式,通常要考虑三方面的问题:
- 单例对象的创建时机,是早点创建,还是晚点创建,这个没有定论,要结合实际自主选择。
- 单例对象在创建过程中的线程安全性,核心就是要保证单例唯一性,避免创建出多个对象。
- 单例对象在使用过程中的线程安全性,这是针对单例对象操作的资源来说,要保证资源的并发安全性。
单例模式的实现
饿汉模式
饿了就想赶快吃东西,表达含义就是要尽早创建单例对象,通常做法是在系统启动过程中就创建好。
模式特点:
- 首次使用单例对象时无需等待创建,可以提高首次使用效率。
- 通常不需要考虑创建过程的线程安全性,因为较早阶段没有使用者,且创建者通常是不会并发操作的。
- 可能会因为单例对象创建耗时,拖慢系统的启动速度。
代码示例,在类加载的时候创建单例对象:
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton() {
}
/**
* 获取单例对象的静态方法
*
* @return 单例对象
*/
public static Singleton getInstance() {
return singleton;
}
}
这段代码里,通过static final
关键字,其实也能保证单例创建过程的线程安全性。
懒汉模式
懒人凡事都会往后拖,表达含义就是要延迟对象创建时机,通常做法是延迟到首次使用时再创建。
模式特点:
- 首次使用时多了创建对象的步骤,所以首次使用对象的效率会降低。
- 创建过程和使用过程都要考虑线程的并发安全性。
- 系统启动速度不会被拖累。
代码示例:在首次使用的时候创建单例对象:
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
/**
* 获取单例对象的静态方法
* 注意!这里的创建过程是线程不安全的!!!
*
* @return 单例对象
*/
public static Singleton getInstance() {
// 如果没有单例对象,就先创建再使用
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
注意,这里的创建过程没有考虑并发性,是线程不安全的!
创建过程的线程安全性
在饿汉模式中,可以通过static final
关键字保证单例的创建安全性,上文已有演示:
private static final Singleton singleton = new Singleton();
在懒汉模式中,有以下三种经典方式可以参考:
方式一,用synchronized
关键字对单例创建方法加锁:
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
方式二,用synchronized
关键字对单例创建代码块加锁,配合双重校验锁定DCL(Double Check Lock)
:
public static Singleton getInstance() {
if (singleton == null) { // 第一次校验
synchronized (Singleton.class) { // 对代码块加锁
if (singleton == null) { // 第二次校验
singleton = new Singleton();
}
}
}
return singleton;
}
方式三,静态内部类,利用内部类延时加载机制达到单例的延时创建效果,不需要锁,非常值得推荐和体会!完整代码:
public class Singleton {
private Singleton() {
}
/**
* 获取单例对象的静态方法
*
* @return 单例对象
*/
public static Singleton getInstance() {
return Holder.singleton;
}
// 静态内部类
private static class Holder {
private static final Singleton singleton = new Singleton();
}
}
使用过程的线程安全性
不同业务场景,单例对象的业务逻辑是不一样的,操作的资源也不同,所以,就笼统的说一下处理思路:
- 确认单例对象操作的资源本身是否能够保证线程安全性,如果能,基本上就没有问题,不需要多做处理。
- 如果单例对象操作的资源本身不能保证线程安全性,就要使用各种锁机制,确保线程安全性。
- 如果操作资源涉及分布式系统,就要考虑分布式锁机制,这个时候有必要回头考虑一下,单例模式用的是否合适。