写此文初衷源于昨晚线上代码抛出的一个空指针异常。单例是一个类,我们希望该类的对象在任意高并发多线程的调用下,只被初始化一次(例如用于系统环境和词典的加载等),后续线程均直接调用即可。我们首先来讲解几种常见的单例模式和其优缺点吧。
1.懒汉式
// 懒汉式,线程不安全
class SingletonDemo {
// 定义一个私有的静态全局变量来保存该类的唯一实例
private static SingletonDemo instance;
private SingletonDemo() {
}
public static SingletonDemo getInstance() {
// 这里可以保证只实例化一次
if (instance == null) { //语句(1)
instance = new SingletonDemo();
}
return instance;
}
}
以上代码很明显不能满足我们的要求。设想有n个线程同时执行语句(1),此时实例还未被初始化,因此均判断为null,于是这n个线程每一个都新建了该类对象。
2.懒汉式改进
// 懒汉式,线程安全,但不高效,因为任何时候只能有一个线程调用getInstance()方法。
class SingletonDemo2 {
private static SingletonDemo2 instance;
private SingletonDemo2() {}
public static synchronized SingletonDemo2 getInstance() { //语句(1)
if (instance == null) { //区域(1)
instance = new SingletonDemo2();
}
return instance;
}
}
我们使用synchronized
来强制使每个线程串行执行语句(1),因此永远只有第一个线程新建了该类对象。那么这段代码缺点在哪呢?速度慢。假设现在有1000个线程,均在语句(1)处排队,当第一个线程创建新对象后,剩下999个线程仍然需要排队,进入区域(1)判断不为空并返回。
这里懒汉式
的意思是:要用的时候才去new。区别于接下来要讲的:
3.饿汉式
/**
* 饿汉式,单例的实例被声明成static和final,在第一次加载到内存中时会初始化。
*
* 缺点:
* 不是一种懒加载(lazy initlalization),在一些场景中无法使用:
* 譬如Singleton实例的创建时以来参数或者配置文件的,在getInstance()之前必须调用
*/
class SingletonDemo5 {
// 类加载时就初始化
private static final SingletonDemo5 instance = new SingletonDemo5();
private SingletonDemo5() {}
public static SingletonDemo5 getInstance() {
return instance;
}
}
这段代码利用了jvm对private static final
只初始化一次的特性,可以解决多线程问题,但是当我们要在getInstance()
前做一些配置工作(例如初始化数据库连接等),那么这种方式就捉襟见肘了。
4.双重检验锁
// 双重检验锁(double checked)
class SingletonDemo3 {
private static volatile SingletonDemo3 instance;
private SingletonDemo3() {}
public static SingletonDemo3 getInstance() {
if (instance == null) { // 区域(1)
synchronized (SingletonDemo3.class) { // 区域(2)
if (instance == null) {
instance = new SingletonDemo3(); // 语句(1)
}
}
}
return instance;
}
}
双重检验锁(double checked)。注意到它有2次判断,一次在同步块内,一次在同步块外。假设现在有4个线程T1,T2,T3,T4。T1,T2进入了区域(1),T3,T4还没启动。T1能进入区域(2)创建instance成功,之后T2进入区域(2),判断非空并出来。此时T3,T4启动了,不会进入区域(1),且无需等待锁。
instance变量声明成volatile
, 它可以禁止指令重排序优化。volatile
的两个作用:
- 禁止指令重排优化。
- 所修饰的变量一旦被修改,其他线程立即可见。(但是非原子操作,即其他线程可以感知到变量被修改,但无法使用
val += 1
这种语句使其原子增加。)因此可用于1读者n写者的场景,例如点击"游戏退出按钮",其他(金币累加/音效)线程将立即感知到。
更多单例模式可以看这里。
5.异常问题回放
昨晚报出异常的代码片段如下:
class WrongSample {
private volatile static WrongSample instance;
private WrongSample() {
}
public static WrongSample getInstance() {
if (instance == null) { // 区域(1)
synchronized (WrongSample.class) { // 区域(2)
if (instance == null) {
instance = new WrongSample(); // 语句(1)
instance.init(); //语句(2)
}
}
}
return instance;
}
private void init() {
}
}
该代码使用双重检验锁构建了一个单例,且对单例进行初始化。那么空指针异常抛出对原因在哪呢?设想现在有线程T1,T2。线程T1进入区域(2),T2此时还未启动。T1执行了语句(1),但并未执行语句2,此时instance已经不是null,所以T2启动时在区域(1)判断非null将直接返回instance,但T2并未被初始化,由是产生异常。
解决方案:初始化操作放进构造函数,执行语句(1)时里暗含里执行构造函数。代码如下:
class WrongSample {
private volatile static WrongSample instance;
private WrongSample() {
init();
}
public static WrongSample getInstance() {
if (instance == null) { // 区域(1)
synchronized (WrongSample.class) { // 区域(2)
if (instance == null) {
instance = new WrongSample(); // 语句(1)
}
}
}
return instance;
}
private void init() {
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。