设计模式(1)-带你了解3类8种单例模式

单例模式的分类

  • 饿汉式
    • 静态常量
    • 静态代码块
  • 懒汉式
    • 线程不安全
    • 线程安全,同步方法
    • 线程安全,同步代码块
    • 双重检查锁
    • 静态内部类
  • 枚举

饿汉式

饿汉式,单例模式的一种类型,对于这个名字可以假想成:

有一天小明买了菜回到家,由于他特别饿,于是就把所有菜都用掉做了满满一桌子菜,而直到最后吃饱,仍然有一些菜从来没尝过,而且由于做的菜太多导致的燃气也用完了。

这里的菜就是我们要使用的对象,而小明就是单例类,燃气就是系统内存。在调用方准备使用对象前,就把所有的对象都实例化好,以供随时调用,但如果实例化工作量过大可能导致内存浪费

饿汉式-静态常量(⭐慎用)

这是最简单的单例模式,主要有以下几点核心思路

  • 私有构造方法
  • 私有静态常量,类加载时初始化常量对象
  • 公有对象获取方法

示例代码如下

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
public class SingletonType01 {
public static void main(String[] args) {
Singleton01 instance1 = Singleton01.getInstance();
Singleton01 instance2 = Singleton01.getInstance();

System.out.println("instance1 == instance2 "+(instance1==instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton01 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton01() {
}
/**
* 在类加载时创建私有的静态变量
*/
private final static Singleton01 INSTANCE = new Singleton01();

/**
* 对外提供获取对象的静态方法,
* 外部调用,类名.方法名 Singleton.getInstance()
* @return 返回单例对象
*/
public static Singleton01 getInstance() {
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 true
491044090
491044090

主方法中对于两次获取到的对象进行了对比,可以看到两者为同一对象,且hashcode相同

优点:

  • 写法简单,在类装载的时候完成实例化,避免线程同步问题

缺点:

  • 在类装载时就实例化,那可能这个对象从始至终都没有被用到,无形中造成资源浪费,没有懒加载效果

这种单例模式,可以使用,并且无需考虑多线程问题,但是存在内存浪费问题

饿汉式-静态代码块(⭐慎用)

饿汉式静态代码块的实现与静态常量基本类似,唯一不同就是对象的实例化从静态变量转移到了静态代码块中,但其都是在类加载是执行的

代码如下

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
42

/**
*
* @author larsCheng
*/
public class SingletonType02 {
public static void main(String[] args) {
Singleton02 instance1 = Singleton02.getInstance();
Singleton02 instance2 = Singleton02.getInstance();

System.out.println("instance1 == instance2 : "+(instance1==instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton02 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton02() {
}
/**
* 静态私有变量
*/
private static Singleton02 INSTANCE;

/**
* 将对象的实例化放在了静态代码块中,同样也是类加载时被执行
*/
static {
INSTANCE = new Singleton02();
}
/**
* 对外提供获取对象的静态方法,
* 外部调用,类名.方法名 Singleton.getInstance()
* @return 返回单例对象
*/
public static Singleton02 getInstance() {
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 : true
491044090
491044090

可以看出同样是单例对象的效果,所有与饿汉式静态常量写法相比较,其优缺点也一样,都会造成内存浪费

懒汉式

前面提到的两种单例模式都是饿汉式,即无论用不用这个对象,他对会被实例化。

这里要提到的是另一种单例模式-懒汉式,即对象只有在需要使用的时候才进行实例化,同样可以想象成一个小场景

有一天小李特别饿,但是他很懒,不想做饭就到餐馆吃饭,看了菜单从里面选择点了一份牛肉拉面,后厨师傅马上给他做好,小李吃饱后就开心的回家了

虽然描述的比较抽象,小李是是对象使用方,菜单上的每一个菜是一个单例类,后厨师傅是JVM。

当你选定一个对象了之后才会为你立即创建,而不是提前把所有的对象都实例化好。这样实现了懒加载的效果

懒汉式-线程不安全(👎👎👎不可使用)

懒汉式的简易版本,这一实现方式虽然做到了懒加载,但是存在线程安全问题

示例代码如下:

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

public class SingletonType03 {
public static void main(String[] args) {
Singleton03 instance1 = Singleton03.getInstance();
Singleton03 instance2 = Singleton03.getInstance();

System.out.println("instance1 == instance2 : " + (instance1 == instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton03 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton03() {
}

/**
* 静态私有变量
*/
private static Singleton03 INSTANCE;


/**
* 对外提供获取对象的静态方法,此处存在线程安全问题
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton03 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton03();
}
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 : true
491044090
491044090

简单的执行测试结果看似乎并无问题,做到了延迟加载(懒加载),并且实现了单例模式

但是!!!这一切都是单线程的前提下,一旦为多线程环境,在getInstance方法中会有严重的线程安全问题

分析:

  • 假设有两个线程A、B
  • A线程先到,判断INSTANCE为空,进入if内,准备进行对象初始化
  • 此时B线程也到达if判断,发现INSTANCE仍为空(A还未完成对象实例化),B也进入if内。

这种情况下,待A、B执行完后,得到的将是两个对象。这就完全违背了单例模式的初衷!!

所以通常情况下,不推荐使用这种懒汉式的单例模式。因为绝大多数的应用场景都为多线程环境。

而在多线程环境下,这种实现方式完全不算单例模式的范畴,因为它会产生多个对象实例

懒汉式 - 同步方法(👎不推荐)

针对于线程不安全问题,对应则有线程安全的解决方案

即在getInstance方法上加入synchronized关键字,将其改造成同步方法,解决在多线程环境下的线程不安全问题

示例代码:

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
class Singleton04 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton04() {
}

/**
* 静态私有变量
*/
private static Singleton04 INSTANCE;

/**
* 对外提供获取对象的静态方法,加入同步关键字,解决线程同步问题
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static synchronized Singleton04 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton04();
}
return INSTANCE;
}
}

如上,虽然解决了线程不安全问题,但是随之而来的是效率问题

分析:

  • 每次调用getInstance方法都需要进行线程同步
  • 实际上造成多个对象被实例化的仅仅只是方法中代码片段

所以总的来说,虽然解决的线程安全问题,但是由于效率不加,且有优化方案,故此种方式也不建议使用

针对同步方法带来的效率问题,有改进方案,但有一种错误的改进方案这里有必要提一下

同步方法改造为同步代码块,尝试减少同步的代码,来提高效率,示例代码如下:

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
class Singleton04ErrorSolution {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton04ErrorSolution() {
}

/**
* 静态私有变量
*/
private static Singleton04ErrorSolution INSTANCE;

/**
* 对外提供获取对象的静态方法,对造成线程安全问题的代码块进行同步
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton04ErrorSolution getInstance() {
if (INSTANCE == null) {
synchronized (Singleton04ErrorSolution.class) {
INSTANCE = new Singleton04ErrorSolution();
}
}
return INSTANCE;
}
}

如上代码的本意是将同步方法细化到同步代码块,来进行效率优化,但是这样的改动起到了相反的效果

分析:

  • 对实例化对象的代码片段进行同步,假设A、B两线程执行getInstance方法
  • A线程判断INSTANCE为空后进入if内,准备执行同步代码块,此时B线程也判断INSTANCE为空,也进入了if内部,等待A线程执行完毕
  • A线程执行完同步代码块后,实例化了一个对象,此时B线程开始执行,也创建了一个对象

从上面的分析可以看出,这种改进方案,属于想法正确,但是操作错误,导致不但没有解决效率问题,同时造成线程安全问题,是一定要避免的错误!!

懒汉式-同步代码块(👎不推荐)

基于上文提到的优化思路:将同步方法细化到同步代码块,那正确的改进方案可能会有下面这种写法:

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

class Singleton05 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton05() {
}

/**
* 静态私有变量
*/
private static Singleton05 INSTANCE;

/**
* 对外提供获取对象的静态方法,加入同步关键字,解决线程同步问题
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton05 getInstance() {
synchronized (Singleton05.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton05();
}
}
return INSTANCE;
}
}

从getInstance方法可以看到,使用了同步代码块的方式,并且同步的是if判断和实例化部分的代码

虽然达到了线程安全,但是基本上和同步方法的效率没什么区别,依旧每个线程进来后,都需要等待执行同步代码块。

这种方案只是为了和上面的错误同步代码块方式进行对比。真实业务中也不推荐使用这种方式!!!

双重检查锁(👍推荐使用)

想要实现懒加载,同时保证线程安全,同时提高效率。那么一起来看看双重检查锁的实现方式:

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
class Singleton06 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton06() {
}

/**
* 静态私有变量
* 声明volatile,防止指令重排,导致的空对象异常
*/
private static volatile Singleton06 INSTANCE;

/**
* 对外提供获取对象的静态方法,使用双重检查锁机制,保证同步代码块中的实例化代码只会被执行一次
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static Singleton06 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton06.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton06();
}
}
}
return INSTANCE;
}
}

首先先来看看该方案于前几种的不同点

  • 使用synchronized关键字实现同步代码块
  • 同步前同步后两次判断
  • 使用了volatile关键字

分析

在getInstance方法中使用了Double-Check概念,配合同步代码块,保证线程安全。简单分析下其流程

  • A、B、C 3个线程执行getInstance方法
  • A、B线程都通过了第一个if判断,A线程抢到了锁,开始执行同步代码块中的逻辑,B等待
  • A通过了第二个if判断,进行了INSTANCE的实例化操作,A完成操作,释放锁
  • B开始执行同步代码块内容,B未通过第二个if(此时的INSTANCE不为空),直接返回INSTANCE对象,B释放锁
  • 此时C开始执行getInstance方法,C未通过第一个if,直接返回INSTANCE对象

从上面分析过程中可以看到,无论有多少个线程,实例化代码只会被执行一次,意味着只会创建一个对象。

volatile

但是在整个流程中有一个小小的隐患

INSTANCE = new Singleton06();它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:

①第一步:给 INSTANCE 分配内存空间;
②第二步:调用 Singleton06 的构造函数等,来初始化 INSTANCE;
③第三步:将 Singleton06 对象指向分配的内存空间(执行完这步 INSTANCE 就不是 null 了)。

这里的理想执行顺序是 1->2->3,实际在Jvm中执行顺序有可能是1->3->2,也有可能是 1->2->3。
这种现象被称作指令重排也就是说第 2 步和第 3 步的顺序是不能保证的,这就导致了隐患的产生。

在线程A执行INSTANCE = new Singleton06();是,JVM中的执行顺序是1->3->2,先进行分配内存再初始化INSTANCE,若在刚完成内存分配时,线程C开始执行第一个if判断,发现INSTANCE不为空,直接返回INSTANCE对象,此时的INSTANCE明显会出现问题。

在Java内存模型中,volatile 关键字作用可以是保证可见性且禁止指令重排。从而避免由于指令重排导致的异常隐患。

关于 volatile关键字和指令重排相关 可以参考此处

总结

双重检测锁的单例实现方案,可以实现延迟加载,同时线程安全并且效率高,在实际场景中是推荐使用的!

静态内部类(👍推荐使用)

除了双重检查锁被推荐使用外,静态内部类实现单例模式也是被推荐使用的一种
示例代码如下:

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

class Singleton07 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton07() {
}

/**
* 提供一个静态内部类,类中声明一个类型为 Singleton07 的静态属性 INSTANCE
*/
private static class SingletonInstance {
private static final Singleton07 INSTANCE = new Singleton07();
}

/**
* 对外提供获取对象的静态方法,
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回静态内部类的静态属性
*/
public static Singleton07 getInstance() {
return SingletonInstance.INSTANCE;
}
}

分析

  • 该方案采用了类装载机制来保证初始化实例时只有一个线程,从而保证了线程安全
  • 单例类Singleton07被装载时,静态内部类SingletonInstance是不会实例化的,只有调用getInstance方法时才会触发静态内部类SingletonInstance的装载,从而执行实例化代码
  • 并且静态内部类的静态属性只会在第一次加载类的时候被初始化,所以做到了懒加载

结论

保证了线程安全,使用静态内部类的特点实现懒加载,并且有较高效率,推荐使用

枚举(👍推荐使用)

那么这么多的实现方案,Java中有没有一个公认的最佳枚举实现方案呢,当然有啊,通过枚举来实现
代码如下:

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 SingletonType08 {
public static void main(String[] args) {
String connection1 = Singleton08.INSTANCE.getConnection();
String connection2 = Singleton08.INSTANCE.getConnection();

System.out.println("connection1 == connection2 : " + (connection1 == connection2));
System.out.println(connection2.hashCode());
System.out.println(connection2.hashCode());
}
}

enum Singleton08 {
/***/
INSTANCE;

/**资源对象,此处以字符串示例*/
private String connection = null;

/**
* 在私有构造中实例化单例对象
*/
Singleton08() {
//模拟实例化过程
this.connection = "127.0.0.1";
}

/**
* 对外提供获取资源对象的静态方法
*/
public String getConnection() {
return connection;
}
}

如上代码是通过枚举来实现单例对象的创建

enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。
枚举类型是线程安全的,并且只会装载一次。

枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,它保证线程安全,并防止外部反序列化的破坏。

---------- 😏本文结束  感谢您的阅读😏 ----------
评论