单例模式引发的思考

单例是个老生常谈的话题,有很多种写法,每一种写起来代码都比较简单,但是每一种背后蕴含的知识可谓是一层套一层,套路很深呀。。。

本篇主要是记录下我对单例的理解。

熟悉单例的人都知道单例一般会有五种写法,分别为懒汉法(线程非安全)、线程安全的懒汉法、饿汉法、内部类法和枚举法

饿汉法

先说个intellij idea中单例模板代码(饿汉法):

1
2
3
4
5
6
7
8
9
10
11
public class SingletonStatic {
private static SingletonStatic singletonStatic = new SingletonStatic();
public static SingletonStatic getSingletonThink(){
return singletonStatic;
}
private SingletonStatic(){
System.out.println("construct function");
}
}

IDE中推荐的是饿汉法,代码是不是很简单。别看代码简单,但功能齐全,此代码是线程安全的。

线程安全是什么?
线程安全是指在多线程中,当多条语句在操作同一个线程的共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来执行,导致共享数据出错。
解决方法:保证多条共享数据的语句,在一个线程完全执行完之前,其他线程不参与执行过程,也就是加锁或者叫同步。

此方式的线程安全是依赖类的加载和初始化实现的。

类加载与初始化
一个类在JVM中被实例化成一个对象,需要经历三个过程:加载、链接和初始化

  • 加载 类在jvm中的加载都是动态加载的,在被首次调用时加载到jvm中,由类加载器将.class文件加载jvm中。
  • 链接 链接简单地说,就是将已经加载的.class组合到JVM运行状态中去。包括验证、准备和解析
  • 初始化 执行类的static块和初始化类内部的静态属性(static块和静态属性是按照声明的顺序初始化的,且仅执行一次),然后是类内部属性,最后是类构造方法。

明白了类的加载和初始化再看上面的代码是不是感觉有点感觉了。

SingletonStatic.getSingletonThink()在jvm中被第一次调用时,SingletonStatic.class被加载到jvm中,然后进行初始化,由于singletonStatic是静态属性,则先被初始化,此时singletonStatic就被实例化,通过getSingletonThink返回。

在多线程中,假如thread1调用SingletonStatic.getSingletonThink(),在其未返回时,thread2也调用SingletonStatic.getSingletonThink(),此时,因为thread1先调用SingletonStatic,被加载到jvm中,初始化之后singletonStatic被实例化,此刻thread2也调用SingletonStatic.getSingletonThink(),jvm发现SingletonStatic已被加载,并且singletonStatic是静态属性,只能被初始化一次并且已在thread1调用时被初始化,则thread2调用时并不会对singletonStatic进行再次初始化,thread1和thread2使用的singletonStatic对象都是同一个,所以线程安全。

代码验证如下:

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
public class MainThread {
public static void main(String[] args) throws InterruptedException {
final Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
SingletonStatic.getSingletonThink(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
SingletonStatic.getSingletonThink(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread.sleep(1000);
thread1.start();
thread2.start();
}
}

执行结果如下:

1
2
3
4
5
Thread-1
Thread-0
construct function
Thread-1 invoke
Thread-0 invoke

其结果显示构造方法只被调用了一次,也就是说singletonStatic被初始化了一次。

懒汉式

上面的代码短小精悍,但其是饿汉式的,单例singletonStatic会在加载SingletonStatic类一开始就被初始化,即使客户端没有调用getSingletonThink()方法,也会被初始化,这就有点不太高效。其实我们是想在用的时候才加载,下面就说下懒汉式。

懒汉式代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SingletonLazy {
private static SingletonLazy singletonLazy ;
private SingletonLazy(){
System.out.println("construct function");
}
public static SingletonLazy getSingletonLazy(){
if (singletonLazy == null){
singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
}

代码也挺短的啊。饿汉式是线程安全的,那我们也来个多线程来测试下,它是否安全。测试代码如下:

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
public class MainThread {
public static void main(String[] args) throws InterruptedException {
final Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
SingletonLazy.getSingletonLazy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
SingletonLazy.getSingletonLazy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread.sleep(1000);
thread1.start();
thread2.start();
}
}

结果如下

1
2
3
4
Thread-0
Thread-1
construct function
construct function

构造方法被调用了两次,非线程安全。那么可以加锁来实现线程安全。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SingletonLazy {
private static SingletonLazy singletonLazy ;
private SingletonLazy(){
System.out.println("construct function");
}
public static SingletonLazy getSingletonLazy(){
synchronized (SingletonLazy.class){
if (singletonLazy == null){
singletonLazy = new SingletonLazy();
}
}
return singletonLazy;
}
}

这样能保证线程安全,但是在每次调用getSingletonLazy时,都会加个锁去判断singletonLazy是否为null,有点不高效呀。

你说每次判断是否为null都加锁,不高效,那就把synchronized放到if里面,这样就不用每次多加锁去判断是否为null了,但是这样并不是线程安全的,有可能thread1和thread2同时判断singletonLazy为null,然后分别对singletonLazy进行实例化。

还有一种方法是在synchronized外面再加个if判断语句,这样每次判断singletonLazy是否为null就不用加锁了,而且当thread1和thread2同时判断singletonLazy为null,而进去最外层的if语句时,当thread1拿到锁之后,会再次判断此时singletonLazy是否依然为null,为null则进行实例化,待singletonLazy实例化之后,释放锁,thread2拿到锁,判断singletonLazy是否为null,singletonLazy已在thread1中被实例化,非null,则在thread2中不会进行实例化,所以线程安全了。此种方法也叫双重校验锁,代码如下:

1
2
3
4
5
6
7
8
9
10
public static SingletonLazy getSingletonLazy(){
if (singletonLazy == null){
synchronized (SingletonLazy.class){
if (singletonLazy == null){
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}

此时就万事大吉了吗?非也!

singletonLazy = new SingletonLazy();这句语义并不是一个原子操作,大概包括3件事,分别为:

  1. 给singletonLazy分配内存
  2. 调用singletonLazy的构造函数来初始化成员变量
  3. 将singletonLazy对象指向分配的内存空间(执行完这步singletonLazy就为非null了)

但是在JVM的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是1-2-3也可能是1-3-2。如果是后者,则thread1在3执行完毕、2 未执行之前,被thread2抢占了,这时singletonLazy已经是非null了(但却没有初始化),所以thread2会直接返回singletonLazy,然后使用,但此时singletonLazy并没有被初始化成功,不能正常使用。

可以在声明singletonLazy静态变量时,使用volatile关键字,volatile是一个轻量的synchronized,具有两重语义,第一是可见性,是指共享变量被修改之后,立马会从本地缓存中写入主内存,以对其它线程可见。第二是禁止指令重排序优化。(这里其实有点以偏概全的意思,当对volatile写时,无论前面的操作是什么,都不能重排序,有的场景中是可以重排序的,具体在这里就不展开了,只是对volatile的写时,前面的代码禁止重排序。)这里使用的就是第二种语义,禁止重排序。

线程安全懒汉式需要注意的几点:

  • synchronized声明为静态属性,且用volatile修饰
  • 双重校验锁(先if判断是否为null,然后加锁,最后再次判断是否为null)
  • 构造方法是私有的

内部类

将懒汉式的代码修补成线程安全之后,发现代码也不短了,而且还比较绕,那么饿汉法能不能优化为懒汉式的呢?答案肯定是可以的,那就是使用内部类,之所以要改成懒汉式,是因为某些场景饿汉式不能使用,如singletonStatic实例的创建是依赖参数或者配置文件的,在getSingletonStatic()之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SingletonInner {
private static class Singleton {
private static SingletonInner singletonInner = new SingletonInner();
}
public static SingletonInner getSingletonInner(){
System.out.println("SingletonInner 已被初始化");
return Singleton.singletonInner;
}
private SingletonInner(){
System.out.println("SingletonInner construct function");
}
}

代码是不是依然很短,而且思路也比较清晰。将singletonInner放在Singleton内部类中,使其不会在SingletonInner初始化时,被实例化,以达到懒加载的效果。

多线程测试代码如下:

1
2
3
4
5
Thread-0
Thread-1
SingletonInner 已被初始化
SingletonInner 已被初始化
SingletonInner construct function

由结果可以推断,SingletonInner被加载并初始化时,singletonInner并没有被实例化,singletonInner的实例化是在调用getSingletonInner之后才调用构造方法进行的实例化,确实是懒加载模式。并且是线程安全的。

枚举式

枚举应该是最简单的,代码如下:

1
2
3
public enum EasySingleton{
INSTANCE;
}

枚举还是线程安全的,因为默认枚举实例的创建是线程安全的,但是在枚举中的其他任何方法由程序员自己负责

总结

实现单例的途径这么多,那么应该首选哪个呢?我比较推荐饿汉式(也是IDE默认推荐的),如果使用场景中需要懒加载,那么可以使用内部类来实现单例。但是线程安全的懒汉式也应该记住。

参考

https://www.oschina.net/question/2273217_217864
http://www.cnblogs.com/yahokuma/p/3668138.html
http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/

您的肯定,是我装逼的最大的动力!