单例模式
# 一、概述
单例模式(Singleton)就是通过一定的方法保证某个类在整个系统中只有一个对象实例,且该类只有一个获取其对象的静态方法。如:一个学校只能有一个正校长,一个省份只能有一个省委书记。
# 1.1 解决了什么问题
单例模式同时解决了两个问题,违反了单一职责原则:
- 保证一个类只有一个实例。
- 为该实例提供一个全局访问节点。
# 1.2 解决方案
保证一个类只有一个实例。
构造函数私有化。
为该实例提供一个全局访问节点。
新建一个静态构建方法作为构造函数。
# 1.3 实现步骤
- 在类中添加一个私有静态成员变量用于保存单例实例。
- 声明一个公有静态构建方法用于获取单例实例。
- 在静态方法中实现"延迟初始化"。该方法会在首次被调用时创建一个新对象,并将其存储在静态成员变量中。此后该方法每次被调用时都返回该实例。
- 将类的构造函数设为私有。类的静态方法仍能调用构造函数,但是其它对象不能调用。
- 检查客户端代码,将对单例的构造函数的调用替换为对其静态构建方法的调用。
# 二、实现方式
单例模式有 8 种实现方式:
- 饿汉式(静态常量)
- 饿汉式(静态代码块)
- 懒汉式(不使用锁)
- 懒汉式(同步方法)
- 懒汉式(同步代码块)
- 双重检查(推荐使用)
- 静态内部类(推荐使用)
- 枚举(推荐使用)
饿汉式的意思就是像饿死鬼一样,不管用不用,上来就先实例化,也不在乎什么浪费不浪费。饿汉式天生就是线程安全的。
懒汉式就是没有预先实例化的这种意识,只有用到了才去实例化。懒汉式天生就是线程不安全的,需要通过各种手段让其线程安全。
# 2.1 饿汉式(静态常量)
/**
* 饿汉式(静态常量)
*/
class Singleton{
private Singleton(){}
private final static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
特点:
- 在类加载的时候完成实例化,可以避免线程同步问题。
- 但是没有达到懒加载的效果,如果整个系统从始至终没有用到过整个实例,则有点浪费。
# 2.2 饿汉式(静态代码块)
/**
* 饿汉式(静态代码块)
*/
class Singleton{
private Singleton(){}
private static Singleton instance;
static {
instance = new Singleton();
}
public static Singleton getInstance(){
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
特点:
和饿汉式(静态常量)的实现方式类似,只是将实例化的过程放在了静态代码块中,可以解决线程同步问题,但是没有达到懒加载的效果,可能会造成内存浪费。
# 2.3 懒汉式(不使用锁)
/**
* 懒汉式(不使用锁)
*/
class Singleton{
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
特点:
- 这种方式实现了懒加载,只有在用到的时候才去实例化。
- 这种方式只能在单线程环境使用,多线程环境下不安全,有可能多个线程同时在执行
new Singleton()
操作。
# 2.4 懒汉式(同步方法)
/**
* 懒汉式(同步方法)
*/
class Singleton {
private Singleton() {}
private static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
特点:
- 解决了线程安全问题,也解决了懒加载的问题。
- 执行效率太低,实际上只有实例化的那部分代码需要线程同步,一旦实例化之后锁就没意义了。如果整个方法都同步的话,那么多个线程都想执行
getInstance()
的时候,同一时刻只有一个线程可以获得。
# 2.5 懒汉式(同步代码块)
/**
* 懒汉式(同步代码块)
*/
class Singleton {
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
特点:
这种方式加了全局唯一锁,看起来是线程安全的,实际上是个陷阱,说到底还是不安全的。在单例对象还未初始化的时候,线程 A 和 B 都是可以执行到if (instance == null) {}
内部的,此时线程 A 拿到了锁,执行instance = new Singleton();
部分,线程 B 在等待。等到线程 A 执行结束之后释放锁,线程 B 也会执行instance = new Singleton();
操作,这样实际上执行了两次实例化操作,获取的是两个不同的对象,所以是不安全的。
# 2.6 双重检查(推荐使用)
推荐
《阿里巴巴 Java 开发手册》约定使用这种方式。
/**
* 双重检查
*/
class Singleton {
private Singleton() {
}
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
为什么需要两个 `if (instance == null)`判断?
如果像下面这样只有一个判断,实际上就是懒汉式(同步代码块)这种方式。在 instance 还未实例化的时候,如果有两个线程执行 if 语句,则两个语句都会进入 if 语句内部,且都会执行实例化操作,只是一前一后执行。synchronized
关键字只是保证在同一时刻只有一个在线程在进行操作。
if (instance == null){
synchronized (Singleton.class) {
instance = new Singleton();
}
}
2
3
4
5
为什么需要 volatile 关键字?
volatile 关键字的作用是禁止指令重排。在使用javap -c -v -p Singleton.class
命令进行反编译,会发现里面有三个主要步骤:
- new:在内存中分配空间用于保存对象(只分配空间,未初始化)。
- invokespecial:调用构造方法,完成单例对象初始化。
- putstatic:将内存地址赋值给成员变量。
上面三个步骤执行完才算是完成了单例对象的初始化,正常顺序应该是从上往下依次执行,但是 Java 虚拟机在执行步骤没有必要关联的情况下是可以重新排序的,重新排序在单线程情况下是没有问题的,因为最终时候的时候都会执行该内存地址。但是在多线程环境下可能会出现问题。
假设在多线程环境下发生了指令重排,其执行顺序为 new、putstatic、invokespecial。线程 A 执行了前两个,该对象已经不是 null,而且 instance 成员变量已经有了内存地址(但是没有调用构造方法)。此时刚好线程 CPU 切换至线程 B 执行,由于线程执行了 putstatic 命令,所以线程 B 认为其已经完成了实例化操作(实际上还未调用构造方法),所以使用是有问题的。
volatile 关键字的作用就是禁止指令重排,让它老老实实按照前后顺序执行。
特点:
- 双重检查模式就是对懒汉式(同步代码块)方式的优化。
- 实现了线程安全、懒加载,且访问效率高。
# 2.7 静态内部类(推荐使用)
/**
* 静态内部类
*/
class Singleton {
private Singleton() {
}
private static volatile Singleton instance;
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static synchronized Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
特点:
利用类加载机制实现了懒加载,同时也实现了线程安全,效率高。
# 2.8 枚举(推荐使用)
public class SingletonTest {
public static void main(String[] args) {
Singleton instance = Singleton.INSTANCE;
}
}
/**
* 枚举
*/
enum Singleton {
INSTANCE;
}
2
3
4
5
6
7
8
9
10
11
12
特点:
利用枚举实现单例,这种思路真的很奇特。使用简单、可以避免线程同步问题、效率又高。
# 三、源码中的应用
# 3.1 java.lang.Runtime
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18