序言
在程序中,有一些对象其实我们只需要一个,比如说:线程池、缓存、处理偏好设置的对象等等。
事实上,这类对象应该只创建一个实例,若制造出多个实例,就会导致许多问题发生,如程序行为异常,资源使用过量或返回的结果不一致。
利用静态类变量、静态方法和适当的修饰符,不是也可以达到这种效果嘛?
确实如此,但其有个缺点,那就是必须在程序一开始就创建好对象,万一这个对象非常耗费资源,而程序又在这次的执行过程中一直没用到它,就会浪费资源。
虽然鱼和熊掌不可兼得,但我们的单例模式却可以分别实现“鱼”或“熊掌”。
简介
单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类 “类 (计算机科学)”)必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常名为 getInstance);当我们调用这个方法时,若类持有的引用不为空就返回这个引用,若类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
由来
问:如何创建一个对象呢?
答:new MyObject() 即可;
问:也就是说,一旦有一个类,就可以多次实例化它,即new MyObject()吗?
答:是的,若是公开的类,就可以。
问:若不是呢?比如默认修饰符的类。
答:那只有同一个包内的类可以实例化它,但其仍可以被实例化多次。
问:那么,使用私有的构造器呢?比如
1 | public MyClass{ |
答:这样定义确实没毛病,但仅有私有构造器的类是不能被实例化的。
问:那有可以使用私有的构造器的对象吗?
答:有,它自己可以调用自己的构造器。。。
问:为什么?
答:因为必须有MyClass类的实例才能调用其的构造器,但因为没有其他类能够实例化MyClass,所以我们得不到这样的实例。这是“鸡生蛋,蛋生鸡”的问题。虽然可以在MyClass类型的对象上使用构造器,但在这之前必须要有一个这个类型的实例,而在产生实例之前,又必须在MyClass实例内才能调用私有的构造器。。。。。
问:那么使用这样的代码如何:
1 | public MyClass{ |
MyClass有一个静态方法,可以通过类名的方式调用:MyClass.getInstance()将这些合在一起,是否就可以初始化一个MyClass呢?比如说这样:1
2
3
4
5
6
7public MyClass{
private Myclass(){}
public static MyClass getInstance(){
return new MyClass();
}
}
答:当然可以,这确实是实例化对象的一种方式,这样MyClass类就可以只创建一个实例了。
类图结构
单例模式:确保一个类只有一个实例,并提供一个全局访问点。
单例模式的类图非常简单,如下图:
类型
具体而言,单例模式可以分为懒汉式和饿汉式两种类型。
懒汉式
懒汉式指的是在只有在第一次使用Singleton.getInstance的时候,才开始实例化该类,即其可以延迟实例化,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Singleton {
private static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
饿汉式
而饿汉式指的是 JVM 的类加载器加载该类的时候就立马实例化该类,代码如下:1
2
3
4
5
6
7
8
9
10public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
多线程下的问题
在多线程中,若当唯一实例尚未创建时有两个线程同时调用创建方法,由于它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。
不过我们有 3 种解决方案:
① 方案一:为了解决这个问题,我们可以在getInstance()方法前面加一个synchronized关键字,将其同步锁起来,这样就不会有两个线程同时进入这个方法。但是同步会降低性能,这又是一个新的问题,而且更严重的是:只有第一次执行该方法时,才真正需要同步,一旦设置了singleton变量,就不再需要同步这个方法了,之后的每次调用,同步都是一个累赘。
当然,若对性能不是很依赖,可以使用同步锁的方式,这样即简单又高效。但若该程序需要使用在频繁运行的地方,这种方式就不合适了。
② 方案二:若应用程序总是立即创建并使用单例,或者在创建和运行方面的负担不太重,就可以摒弃“懒汉式”的方式,使用“饿汉式”的单例模式!这样在一开始的时候就实例化了该类,不会纠结该不该用同步锁的问题。
③ 方案三:用“双重检查加锁”,在getInstance()中减少使用同步。利用双重检查加锁,首先检查是否实例以及创建了,若尚未创建,才进行同步,只有第一次会同步,若性能是关注的重点,这个做法可以大大地减少getInstance()的时间耗费,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Singleton {
// 使用 volatile 关键字,确保当 singleton 变量被初始化为 Singleton 实例时,多个线程正确的处理 singleton 变量。
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
// 只有第一次才彻底执行下面 if 语句的代码
if (singleton == null) { // 检查实例是否存在,若不存在,进入同步区块
synchronized (Singleton.class) {
if (singleton == null) { // 再检查一次,若仍是 null,才创建实例
singleton = new Singleton();
}
}
}
return singleton;
}
}
疑问
为什么 singleton 变量使用 volatile 关键字
在一些低版本的 Java 里,由于指令重排的缘故,可能会导致单例被 new 出来后,还没来得及执行构造函数,就被其他线程使用。 这个关键字,可以阻止字节码指令的重排序
为什么需要第一次检查 singleton == null?
第一次检查是为了避免每次调用 getInstance() 方法时都进入同步块。synchronized 会引起性能问题,因为它会阻塞其他线程的访问。当 singleton 已经被创建并且不为 null 时,直接返回实例即可,避免了进入同步代码块,从而提高了性能。
为什么还要第二次检查 singleton == null?
第二次检查是为了避免多个线程重复创建实例。
在多线程环境中,假设有多个线程同时执行到第一次检查 singleton == null,并且都通过了检查(因为此时 singleton 仍然是 null)。然后,这些线程会进入同步代码块,但实际上只有一个线程会执行 singleton = new Singleton() 来创建单例实例。其他线程将等待锁释放,但在这个过程中,可能会出现以下问题:
- 线程 A 创建了 singleton 对象,并完成了构造器的执行。
- 线程 B、C 等等也进入了同步代码块,并且尝试创建 singleton。
为了避免多个线程重复创建实例,需要在同步块内再次检查 singleton == null。这个第二次检查保证了即使有多个线程通过了第一次的 null 判断,也只有第一个线程会创建实例,其他线程会看到已经初始化好的 singleton 实例并直接返回。
参考
- Eric Freeman. HeadFirst 设计模式 [M]. 中国电力出版社,2007
文章信息
| 时间 | 说明 |
|---|---|
| 2019-03-23 | 初稿 |
| 2023-01-13 | 微调 |