1. 单例模式优点
- 能够避免对象重复创建,节约空间并提升效率
- 避免由于操作不同实例导致的逻辑错误
2. 饿汉式
特点:单例变量在声明时就初始化
实现
1
2
3
4
5
6
7
8
9
10
11public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}分析
- 优点:简单直观
- 缺点:占用内存,增加类初始化时间
3. 懒汉式(一):原教旨
特点:先声明一个空单例变量,需要用时再初始化
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}分析
- 优点:按需加载,避免了内存浪费,减少了类初始化时间
- 缺点:线程不安全。如果有多个线程同一时间调用
getInstance()
方法,instance 变量可能会被实例化多次
4. 懒汉式(二):线程安全
特点:给判空过程加锁
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}分析
- 优点:能保证多个线程调用
getInstance()
时,一次最多只有一个线程能够执行判空并 new 出实例的操作,所以 instance 只会实例化一次,实现线程安全 - 缺点:当多个线程调用
getInstance()
时,每次都需要执行 synchronized 同步化方法,严重影响程序的执行效率
- 优点:能保证多个线程调用
5. 懒汉式(三):双重校验锁 + volatile
特点:在同步化之前,再加上一层检查
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class Singleton {
// TODO: 加 volatile 关键字优化:禁止 JVM 底层指令重排,避免特殊情况下的空指针异常
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
// 注意内部的判空不能去掉,即使外部有了一层判空
// 因为如果里面不做检查,可能会有两个线程同时通过了外面的判空检查
// 两个线程分别进入锁中进行 new 操作,导致创建多个实例
if (instance == null) {
instance = new Singleton();
}
}
}
reurn instance;
}
}分析
- 优点:如果 instance 已经被实例化,则不会执行同步化操作,大大提升了程序效率
- 缺点:JVM 底层为了优化程序执行效率,可能会对代码进行指令重排,导致在一些特殊情况下出现空指针。为了防止这个问题,更进一步的优化是给 instance 变量加上 volatile 关键字
6. 懒汉式(四):静态内部类
特点:可以实现懒加载,可以保证线程安全
实现
1
2
3
4
5
6
7
8
9
10
11
12
13public class Singleton {
private static class SingletonHolder {
public static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}分析
静态内部类方式是怎么实现懒加载的?
- Java 类加载过程包括:加载、验证、准备、解析、初始化。初始化阶段即执行类的 clinit 方法(clinit = class + initialize),包括为类的静态变量赋初始值和执行静态代码块中的内容。但不会立即加载内部类,内部类会在使用时才加载。所以当外部类 Singleton 加载时,内部类 SingletonHolder 并不会被立即加载,所以不会像饿汉式那样占用内存
- Java 虚拟机规定,当访问一个类的静态字段时,如果静态字段所属的类尚未初始化,则立即进行初始化。当调用外部类 Singleton 的
getInstance()
方法时,由于其使用了内部类 SingletonHolder 的静态变量 instance,所以这时才会去初始化内部类 SingletonHolder,在 SingletonHolder 中 new 出 Singleton 对象,这样就实现了懒加载
静态内部类方式是怎么保证线程安全的?
- Java 虚拟机的设计是非常稳定的,早已考虑到了多线程并发执行的情况。虚拟机在加载类的 clinit 方法时,会保证 clinit 方法在多线程中被正确地加锁、同步
- 即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行 clinit 方法,其他线程都需要阻塞等待,从而保证了线程安全
7. 饿汉式和懒汉式使用场景的一般建议
- 饿汉式:构建不复杂,加载完后会立即使用的单例对象
- 懒汉式:构建过程耗时较长,并不是所有使用此类都会用到的单例对象
8. 双重校验锁单例模式中,volatile 主要用来防止哪几条指令重排序?如果发生了重排序,会导致什么样的错误
instance = new Singleton();
,在这一行代码中,执行了三条重要指令- 分配对象的内存空间
- 初始化对象
- 将变量 instance 指向刚分配的内存空间
如果第二条指令和第三条指令发生了重排序,可能导致 instance 还未初始化时,其他线程提前通过双重校验锁的外层 null 检查,获取到不为 null,但还没执行初始化的 intance 对象,导致空指针异常