单例模式,顾名思义就是只有一个实例,并且它自己负责创建自己的对象,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。下面我们来看下有哪几种实现方式吧。

单例模式分饿汉式和懒汉式,下面介绍下两种的具体写法:

饿汉式

饿汉式就是在类加载的时候就已经创建实例,不管你用没用到,都创建。

好处:线程安全

坏处:浪费内存空间

1
2
3
4
5
6
7
8
9
10
11
12
public class Hungry {

//构造器私有
private Hungry() {
}

private static Hungry hungry = new Hungry();

public static Hungry getInstance(){
return hungry;
}
}

我们可以简单的测试下,它在多线程环境下是否会产生线程安全问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestHungry {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(8);
for (int i = 0; i < 1000; i++) {
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+Hungry.getInstance());
}
});
}
pool.shutdown();
}
}

pool-1-thread-8design.singleton.Hungry@fa49b1e
pool-1-thread-7design.singleton.Hungry@fa49b1e
pool-1-thread-8design.singleton.Hungry@fa49b1e
pool-1-thread-5design.singleton.Hungry@fa49b1e
pool-1-thread-1design.singleton.Hungry@fa49b1e

由于打印数据过多,我们只截取一部分,发现他们的hashcode都是一样的,说明他们都是同一个实例对象

那为什么这种方法就能实现线程安全呢?

类加载的方式是按需加载,且只加载一次。

因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。单例就是该类只能返回一个实例。

换句话说,在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例。

也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。

懒汉式

懒汉式,顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。有线程安全和线程不安全两种写法。

Lazy01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 懒汉式单例01
* 单例实例在第一次使用的时候进行创建,这个类是线程不安全的
* @author 路飞
* @create 2021/3/16 12:25
*/
public class Lazy01 {

private Lazy01(){}

private static Lazy01 instance = null;

public static Lazy01 getInstance(){
//多个线程调用,可能会创建多个对象
if (instance == null){
instance = new Lazy01();
}
return instance;
}
}

我们通过测试,发现多线程环境下,拿到实例的hashcode有的是不一样的,就说明它并不能保证线程安全。

Lazy02

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 单例实例在第一次使用的时候进行创建,这个类是线程安全的,但是这个写法不推荐
* @author 路飞
* @create 2021/3/16 14:59
*/
public class Lazy02 {

private Lazy02(){}

private static Lazy02 instance = null;

public static synchronized Lazy02 getInstance(){
if (instance == null){
instance = new Lazy02();
}
return instance;
}
}

可以看到在获取实例时,该类在方法是加入synchronized以保证线程的有序性,但synchronized是重量级锁,效率并不高。

Lazy03

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
36
37
38
39
40
41
/**
* 懒汉模式(双重锁同步锁单例模式),单例实例在第一次使用的时候进行创建,但是,
* 这个类不是线程安全的!!!!!
* @author 路飞
* @create 2021/3/16 15:01
*/
public class Lazy03 {

private Lazy03(){}

private static Lazy03 instance = null;

//线程不安全
//当执行instance = new Lazy04();这行代码时,CPU会执行如下指令:
//1.memory = allocate() 分配对象的内存空间
//2.ctorInstance() 初始化对象
//3.instance = memory 设置instance指向刚分配的内存
//单纯执行以上三步没啥问题,但是在多线程情况下,可能会发生指令重排序。
// 指令重排序对单线程没有影响,单线程下CPU可以按照顺序执行以上三个步骤,但是在多线程下,如果发生了指令重排序,则会打乱上面的三个步骤。
//如果发生了JVM和CPU优化,发生重排序时,可能会按照下面的顺序执行:
//1.memory = allocate() 分配对象的内存空间
//3.instance = memory 设置instance指向刚分配的内存
//2.ctorInstance() 初始化对象
//假设目前有两个线程A和B同时执行getInstance()方法,A线程执行到instance = new
//SingletonExample4(); B线程刚执行到第一个 if (instance == null){处,
//如果按照1.3.2的顺序,假设线程A执行到3.instance = memory 设置instance指向刚分配的内
//存,此时,线程B判断instance已经有值,就会直接return instance;
//而实际上,线程A还未执行2.ctorInstance() 初始化对象,也就是说线程B拿到的instance对象
// 还未进行初始化,这个未初始化的instance对象一旦被线程B使用,就会出现问题。

public static Lazy03 getInstance(){
if (instance == null){
synchronized (Lazy03.class){
if (instance == null){
instance = new Lazy03();
}
}
}
return instance;
}
}

注释说的很详细了,就是由于指令的重排,照成代码执行顺序的不一致,可能未初始化取返回给需要实例的类,就会造成安全问题。

指令重排会发生在编译器或指令并行或者操作系统中,虽然这个概率很小,但并不不能说该单例模式是线程安全的。

Java中有Volatile关键字,就可以禁止指令重排,具体原因,是操作系统的知识了(本菜鸟现在也没学),以后再深究。

Lazy04

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* (双重锁同步锁单例模式)单例实例在第一次使用的时候进行创建,这个类是线程安全的,使
* 用的是 volatile + 双重检测机制来禁止指令重排达到线程安全
* @author 路飞
* @create 2021/3/16 15:07
*/
public class Lazy04 {

private Lazy04(){}

private volatile static Lazy05 instance = null;

public static Lazy04 getInstance(){
if (instance == null){
synchronized (Lazy04.class){
if (instance == null){
instance = new Lazy05();
}
}
}
return instance;
}
}

加入valitile,禁止指令重排,实现线程安全!

Lazy05

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 单例实例在类装载的时候(使用静态代码块)进行创建,是线程安全的
* @author 路飞
* @create 2021/3/16 15:09
*/
public class Lazy05 {

private Lazy05 (){}

private static Lazy05 instance = null;

static {
instance = new Lazy05();
}

public static Lazy05 getInstance(){
return instance;
}
}

静态代码块,在项目运行时只加载一次,可能和饿汉式有点像,都是借助类加载只会加载一次的特点。

Lazy06

以上在懒汉单例模式中实现了线程安全,但都会被万恶之源反射给攻破,所以引入枚举来实现最终的单例模式

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
/**
* 枚举方式进行实例化,是线程安全的,此种方式也是线程最安全的
* @author 路飞
* @create 2021/3/16 15:11
*/
public class Lazy06 {

private Lazy06(){}

public static Lazy06 getInstance(){
return Lazy.INSTANCE.getInstance();
}

private enum Lazy{
INSTANCE;

private Lazy06 singleton;

//JVM保证这个方法只会被调一次
Lazy(){ //构造方法私有
singleton = new Lazy06();
}

public Lazy06 getInstance(){
return singleton;
}
}

具体为什么能实现避免反射的问题,下面的博文讲的很好:

为什么要用枚举实现单例模式(避免反射、序列化问题)

总结

在写饿汉式单例模式的时候,我建议可以写Lazy04和Lazy06,前者双重检测+volatile,后者是JDK5引入枚举后写单例的方式,都需要掌握。