单例模式(Singleton)

单例模式的介绍、使用场景、七种写法及测试

概述

单例模式就是:确保一个类只有一个实例,并提供该实例的全局访问点。

使用场景

  1. 平常写代码的时候全局属性的保存(有状态的工具类对象,如使用 excel 导出带下拉的时候,需要一个 handler 缓存下拉值,这个下拉值需要全局使用)
  2. 多个模块使用同一个数据源连接对象
  3. 多线程的线程池也是被设计为单例,方便对池中现成进行控制

适用场景

  1. 需要频繁实例化然后销毁的对象
  2. 创建对象耗时过多或耗资源过多,但是又会经常用到的对象
  3. 方便资源相互通信的环境

实现方式

是否必须用单例?

在不需要维持任何状态下,仅仅用于全局访问,这个使用使用静态类的方式更加方便;但如果需要被继承以及需要维持一些特定状态的情况下,就适合使用单例模式,以下这种方式在我们开发中非常常见。

1
2
3
4
5
public class Singleton00 {

public static Map<String,String> cache = new ConcurrentHashMap<>();

}

单例的七种实现方式

并发环境测试代码

因为涉及到判断我们使用的单例模式是否是线程安全的,所以我们需要一个并发环境来测试,如下是具体代码,整体思路是,设置一个计数器,然后启动一个线程,计数器减一,当计数器为0时,所有线程同时启动,通过 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
28
29
30
31
public class Main {
public static void main(String[] args) throws InterruptedException {

int threadNumber = 100;
CountDownLatch start = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(threadNumber);
ExecutorService executorService = Executors.newFixedThreadPool(threadNumber);
for (int i = 0; i < threadNumber; i++) {
executorService.submit(() -> {
try {
// 先阻塞这别让这个线程跑起来
start.await();
// 创建实例
Singleton01.getInstance();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 一个线程跑完 end计数器-1
end.countDown();
}
});
}

// start-1 所有线程启动,模拟并发
start.countDown();
// 阻塞直到执行完毕
end.await();
executorService.shutdown();

}
}

懒汉式(线程不安全)

优点:延迟实例化,用到对象的时候再new

缺点:多线程环境下,线程不安全,多个线程同时进入 getInstance 方法,并且都判断自己为 null ,所以就会出现多次 new Singleton01() 的情况 【不推荐】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton01 {

private static Singleton01 instance;

private Singleton01() {
System.out.println("Singleton01实例化");
}

public static Singleton01 getInstance() {
if (null != instance) {
return instance;
}
instance = new Singleton01();
return instance;
}
}

并发环境下获取实例测试,会打印多次,也就是会构造多个对象

1
2
3
4
5
6
Singleton01实例化
Singleton01实例化
Singleton01实例化
Singleton01实例化
Singleton01实例化
Singleton01实例化

懒汉式(线程安全)

既然上述 getInstance() 的方法不安全,那就加个锁好了,别让线程都进来,没抢到的等着就行

但是,每次访问 getInstance() 都需要锁占用导致资源浪费 【不推荐】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton02 {

private static Singleton02 instance;

private Singleton02() {
System.out.println("Singleton02实例化");
}

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

并发测试:

1
Singleton02实例化

饿汉式(线程安全)

上述线程不安全的问题主要是由于实例化了多次,我们这次类加载的时候就直接实例化,从而避免了实例化多次的情况发生,但是也不能节约资源了【不推荐】

1
2
3
4
5
6
7
8
9
10
11
public class Singleton03 {
private static final Singleton03 INSTANCE = new Singleton03();

private Singleton03() {
System.out.println("Singleton03实例化");
}

public static Singleton03 getInstance() {
return INSTANCE;
}
}

使用类的内部类(线程安全)

因为用的时候才会加载内部类,jvm 可以保证多线程下类的<clinit>只会执行一次,其他线程都会阻塞等待

既实现了延迟加载,又实现了线程安全【推荐使用】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton04 {

private static class SingletonHolder {
private static final Singleton04 INSTANCE = new Singleton04();
}

private Singleton04() {
System.out.println("Singleton04实例化");
}

public static Singleton04 getInstance() {
return SingletonHolder.INSTANCE;
}
}

双重校验锁(线程安全)

双重锁的方式是方法级锁的优化,减少了部分获取实例的耗时。也满足了懒加载

这里为什么采用 volatile 关键字修饰实例对象?

因为instance = new Singleton05();这段代码其实分三步执行:

  1. 为 instance 分配内存空间
  2. 初始化 instance
  3. 将 instance 指向分配的内存地址

由于 JVM 具有指令重排的特性,执行顺序也有可能变为1 -> 3 -> 2。指令重排在单线程环境下不会出现问题,但是在多线程环境下,会出现一种情况,一个线程 A 执行了 1 和 3,也就是分配了内存空间,还把这个对象指向了那个内存空间了,但是就是还没初始化呢,另一个线程 B 进入 getInstance 方法,一看对象不是空的,就直接返回了,但是此时返回这个是没有初始化的对象。

volatile 关键字可以禁止 JVM 的指令重排功能,保证多线程环境下也可以正常运行。

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

private Singleton05() {
System.out.println("Singleton05实例化");
}

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

CAS「AtomicReference」(线程安全)

java 并发库提供了很多原子类来支持并发访问的数据安全性;AtomicIntegerAtomicBooleanAtomicLongAtomicReferenceAtomicReference<V> 可以封装引用一个V实例,支持并发访问。

使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton06 {

private static final AtomicReference<Singleton06> INSTANCE = new AtomicReference<>();

private static Singleton06 instance;

private Singleton06() {
System.out.println("Singleton06实例化");
}

public static final Singleton06 getInstance() {
for (; ;) {
Singleton06 instance = INSTANCE.get();
if (null != instance) {
return instance;
}
INSTANCE.compareAndSet(null, new Singleton06());
return INSTANCE.get();
}
}
}

枚举(线程安全)

枚举有两种方式,一种是新建一个类,就是来做单例的;

还有一种是对已有类改造为单例模式的场景。

枚举实现单例好处:这种方式解决了最主要的;线程安全、自由串行化、单一实例。

可以防止反射攻击

  1. 新建一个类做单例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public enum Singleton071 {
    /** 实例 */
    INSTANCE;

    public void businessMethod() {
    System.out.println("业务方法~");
    }

    public static void main(String[] args) {
    Singleton071 instance = Singleton071.INSTANCE;
    Singleton071 instance1 = Singleton071.INSTANCE;
    // true
    System.out.println(instance1 == instance);
    }
    }
  2. 对已有类改造为单例

    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
    public class Singleton072 {

    private Singleton072() {
    System.out.println("Singleton072实例化");
    }

    private enum SingletonEnum {
    /** 枚举对象 */
    INSTANCE;

    private final Singleton072 instance;

    SingletonEnum() {
    instance = new Singleton072();
    }

    private Singleton072 getInstance() {
    return instance;
    }
    }

    public static Singleton072 getInstance() {
    return SingletonEnum.INSTANCE.getInstance();
    }
    }