Java 单例模式

介绍

什么是单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点

解决什么问题:省略创建对象所花费的时间,不需要频繁创建对象,减轻 GC 压力。

单例模式有以下几种实现方式:

线程安全并发性能好可以延迟加载序列化/反序列化安全能抵御反射攻击
饿汉式YY
懒汉式-不加锁YY
懒汉式-加锁YY
双重检查 Double CheckYYY
静态内部类YYY
枚举YYYY

懒汉式

  • 第一次使用的时候才进行加载
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 Singleton {

private Singleton(){}

private static Singleton singleton;

public static Singleton getSingleton(){

if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}

// 加锁线程安全,但是每次获取都会加锁判断
public class Singleton {

private Singleton(){}

private static Singleton singleton;

public static synchronized Singleton getSingleton(){

if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}

饿汉式

  • 在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。

  • 通过类加载机制保证单例,但是如果代码中有其它方式导致类加载(反射、反序列化),就不满足单例

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

public static Singleton singleton = new Singleton();

private Singleton(){}

public static getSingleton() {
return singleton;
}

}

双重校验(double check)

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

private Singleton(){}

public static volatile Singleton singleton;

public static getSingleton(){
if (singleton == null){
synchronized(Singleton.class){
if (singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
  1. 第一次校验 singleton 是否为空,是为了提高代码执行效率。单例模式只创建一次,后面调用就不用加锁直接返回已创建的实例。

  2. 第二次校验 singleton 是否为空,是防止二次创建实例。线程 A 、线程 B 同时进入第一个判断之后,线程 B 拿到锁创建了实例,然后线程 A 拿到锁之后如果不加以判断,就会再次创建实例。

  3. singleton 用 volatile 修饰是为了防止 JVM 指令重排序。singleton = new Singleton() 分为以下 3 步,步骤 3 可能在步骤 2 之前执行,此时另外的线程发现 singleton 不为 null,直接跳过第一个判断,返回未初始化完全的对象就会出问题。

    1. 分配内存空间

    2. 初始化对象

    3. 内存空间赋值给对应的引用

静态内部类

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

private Singleton (){}

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

public static final Singleton getSingleton(){
return SingletonHolder.INSTANCE;
}
}
  • 满足延迟加载:Singleton 类被装载了,instance 不一定被初始化,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类。

    对于类的初始化,虚拟机规范则严格规定了有且只有四种情况必须立即对类进行初始化,遇到 new、getStatic、putStatic 或 invokeStatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

    生成这4条指令最常见的 java 代码场景是:

    1)使用 new 关键字实例化对象
    2)读取一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
    3)设置一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
    4)调用一个类的静态方法
    
  • 执行类的初始化期间,JVM 会获取一个锁,同步多个线程对同一个类的初始化,所以可以保证线程安全

  • 这种方式只适用于静态域的情况

枚举

1
2
3
4
5
6
public enum Singleton {
INSTANCE;

public void doSomeThing() {
}
}

参考

双重检查锁定与延迟初始化

面试官所认为的单例模式