一、基本介绍
1.1 概念
单例模式,即单个实例的模式,开发中非常常见,比如全局对象管理、工具类等基本都会使用单例模式。
1.2 优缺点
优点:
- 节约资源:不用频繁创建对象,减少了系统开销。
- 方便访问:提供全局统一访问点,便于调用。
缺点:
- 由于单例对象一般都是长生命周期对象,使用不当很有可能导致内存泄漏(单例对象持有短生命周期对象导致其无法被回收)。
二、实现
1.1 饿汉式
听这个名字可能一头雾水,可以这样想:类似于一个"饥饿"的人,在程序启动时就要马上吃饭(创建实例),不管后续是否会真正需要使用这个实例
。
实现如下:
public class Singleton {
// 类加载的时候就new一个对象
private static Singleton INSTANCE = new Singleton();
// 私有构造器,防止外部创建实例
private Singleton() {}
// 全局统一访问点
public static Singleton getInstance() {
return INSTANCE;
}
}
饿汉式是最简单的一种单例实现方式,但是同时也带来了资源浪费的问题,上面也说了,不管后续有没有使用到该类实例,实例都已经被创建了,这显然是不太优雅的。
1.2 懒汉式
由于饿汉式不够优雅,懒汉式应运而生,这个名字更好理解,可以想象成这个人很"懒",只有
在饿得不行的时候才吃饭(创建对象)。
实现如下:
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
这样就优雅不少了,只有在真正需要的时候,这个单例对象实例才会被创建。
但是,优雅仅限于单线程的情况,在多线程环境下,上面方法并不安全。
比如,此时有A
、B
两线程同时进入getInstance()方法,并且此时INSTANCE
实例之前还未被创建过,就会出现如下情况:
- 线程A首先判断INSTANCE为null,然后开始创建新的实例。
- 在线程A还未完成实例化之前,线程B也判断INSTANCE为null,然后也开始创建新的实例。
- 最终,线程A和线程B分别创建了各自的实例,导致系统中存在
多个实例
,违背了单例模式的初衷。
1.2.1 懒汉式优化①
不就是多线程嘛,加锁!由此,你得出了第一种优化方案,给getInstance()方法加锁
:
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
// 保证每次只有一个线程进入getInstance()方法
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
这样不就好了吗,即实现了懒加载,又实现了线程安全。
但是,性能呢?上面这个锁明显属于粗粒度锁
,以后每次访问都需要先获取锁才能进入getInstance()方法,显然是一笔巨大的性能开销。
1.2.2 懒汉式优化②
思考之后,你得出了第二种优化方案,即只对方法中的创建实例的局部代码加锁
,也就是降低锁的粒度:
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) { // --> 位置1
INSTANCE = new Singleton(); // --> 位置2
}
}
return INSTANCE;
}
}
这样的话,每次线程进来会先获取锁,然后只有一个线程去执行同步代码块,创建INSTANCE实例。
但是,上面这样写对吗?不妨举个例看一下:
- 线程A、B同时进入getInstance()方法,由于是第一次进入,INSTANCE还未实例化,二者都到了位置1。
- 假设线程A抢到了锁,到达
位置2
,创建完对象之后退出同步代码块释放锁。 - 接下来线程B拿到锁,到达
位置2
,此时线程B并不知道A已经创建了实例,仍会创建另一个对象实例。 - 最终线程A和线程B分别创建了各自的实例,导致系统中存在
多个实例
,完了,又搞回去了。
1.2.3 懒汉式优化③
不过这次简单,再加个判空就行了嘛,于是有了双重检查锁定(double-checked locking)
:
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
nice!大功告成!欸等等,怎么第一个if
那里有个警告?点开灯泡看一下:
![](https://i-blog.csdnimg.cn/blog_migrate/3d889b42507b27b15fcf4e6218a18ed4.png)
哦?IDE 已经发现我们写出了一段双重检查锁定
的代码,并贴心的给出了提示信息。
按照提示信息,我们在INSTANCE
变量声明时加上volatile
:
public class Singleton {
private static volatile Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
警告消失!至此,终于是完成了单例模式的懒汉式实现。
最后留下一个问题,就是volatile
在这里究竟是起到了防止指令重排
,还是可见性
的效果?还是两者都有?网络上对此也是各种说法,我个人更倾向于可见性一点,欢迎留言一起讨论!