单例模式是Java中最简单的设计模式之一,属于创建型模式,提供了一种创建对象的方式。单例模式主要解决的是,⼀个全局使⽤的类频繁的创建和消费,从⽽提升提升整体的代码的性能。

Singleton Pattern

单例设计模式是最简单的设计模式之一,其实现方式也多种多样。

1、单例的实现方式

单例的实现方式较多,主要从实现上分为是否支持懒汉式、是否线程安全等。

1.1 饿汉式-静态变量-线程安全

1
2
3
4
5
6
7
8
9
10
public class Singleton01 {
// 私有化构造器,防止被手动创建
private Singleton01() {}
// 在类【初始化】阶段调用执行创建,由JVM保证线程安全
private static Singleton01 instance = new Singleton01();
// 对外获取单例对象的方法
public static Singleton01 getInstance() {
return instance;
}
}

new Singleton01会在类初始化阶段被创建:

image-20210312184627616

<clinit>会在类初始化阶段被执行,对于<clinit>()方法的调用,也就是类的初始化,虚拟机会在内部确保线程安全性。虚拟机会确保<clinit>()在多线程环境下能被正常的加锁、同步。如果多线程同时初始化一个类,只能有一个线程执行该类的<clinit>()方法。

1.2 懒汉式-内部类-线程安全

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton02 {
// 私有静态内部类
private static class SingletonHolder {
private static Singleton02 instance = new Singleton02();
}
// 私有化构造器
private Singleton02() {}
// 对外访问的方法
public static Singleton02 getInstance() {
return SingletonHolder.instance;
}
}

使⽤类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的⽅式耗费性能。

这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是⼀个类的构造⽅法在多线程环境下可以被正确的加载。

此种⽅式也是⾮常推荐使⽤的⼀种单例模式。

1.3 懒汉式-双重校验锁-线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton03 {
private static volatile Singleton03 instance;

private Singleton03() {
}
// 双重校验
private static Singleton03 newInstance() {
if (instance == null) {
synchronized (Singleton03.class) {
if (instance == null) {
instance = new Singleton03();
}
}
}
return instance;
}
}

为什么使用volatile?

instance = new Singleton03();在底层会拆分为以下几个步骤:

a. 申请对象的内存空间

b. 初始化内存空间

c. 将instance指向分配的内存空间

由于存在指令重排序,b与c的执行顺序可以不一致,可能出现一下顺序:

a. 申请对象的内存空间

c. 将instance指向分配的内存空间

b. 初始化内存空间

这时就会出现instance不是null,而指向的地方却仍未被初始化。

假想一下情况,A线程刚好执行到c,这时刚好系统调度,B线程判断instance不是null,获取到了instance,并通过instance,而instance其实未被初始化完成,这就会出现安全问题。使用volatile修饰可以避免该变量在操作时发生指令重排序,可以避免以上隐患的发生。

1.4 懒汉式-CAS-线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton04 {
private static final AtomicReference<Singleton04> INSTANCE = new AtomicReference<>();
private static Singleton04 instance;
private Singleton04() {
}
public static final Singleton04 getInstance() {
while (true) {
Singleton04 instance = INSTANCE.get();
if (null != instance) return instance;
INSTANCE.compareAndSet(null, new Singleton04());
return instance;
}
}
}

java并发库提供了很多原⼦类来⽀持并发访问的数据安全 性; AtomicInteger 、 AtomicBoolean 、 AtomicLong 、 AtomicReference 。

AtomicReference 可以封装引⽤⼀个V实例,⽀持并发访问如上的单例⽅式就是使⽤了这样的⼀个特点。

使⽤CAS的好处就是不需要使⽤传统的加锁⽅式保证线程安全,⽽是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。

相对于其他锁的实现没有线程的切换和阻塞也就没有了额外 的开销,并且可以⽀持较⼤的并发性。

当然CAS也有⼀个缺点就是忙等,如果⼀直没有获取到将会处于死循环中。

1.5 饿汉式-枚举-线程安全

1
2
3
public enum Singleton05 {
INSTANCE;
}

Effective Java作者推荐,枚举类型是线程安全的,由JVM保证,枚举的写法非常简单,而且枚举类型是 所用单例实现中唯一一种不会被破坏的单例实现模式。

2、单例的使用场景

单例的使用场景随处可见,JDK中Runtime类就实现了单例设计模式。

image-20210312203532350

除此之外,单例还被利用在Spring中⼀个单例模式bean的⽣成和使⽤。

3、总结

单例模式的实现可以分为线程是否安全、是否懒加载。

  • 懒加载
    • 双重检查锁:synchronized代码块代替synchronized方法,省去不必要的锁,大大提高性能
    • 内部类:既实现了懒加载,又不用加锁
    • CAS:利用了CAS的特性,不加锁,但是循环太久会浪费资源
  • 饿汉式
    • 静态变量实现:利用类【初始化】会执行<clinit>实现实例的创建
    • 枚举:JVM提供线程安全支持,Effective Java作者推荐

之所以区分懒加载,一部分原因就是因为一些使用周期比较短的类,如果过早的被创建而却不被使用,会存在内存的浪费(有些人也称为广义上的内存泄漏),因此对于懒饿的使用应该根据使用场景而定,比如JDK的Runtime由于一启动就要使用,因此使用饿汉式来实现。

4、参考

评论