1. 写法
写法(一):懒汉式(线程不安全)
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Singleton {
private static Singleton instance;
// 私有的构造方法,这样除了它本身,不会被其他类实例化
private Singleton() {}
// 单例模式中,getInstance() 的写法更常见;Java 源码中 newInstance() 的写法更常见。而且从语义上来说,getInstance() 的方法名更贴近“单例”的含义
// 通过此静态方法提供全局获取唯一可用对象的访问点
public static Singleton getInstance() {
if(instance == null) { // 当多个线程执行到这行代码时会产生多个实例
intance = new Singleton();
}
return instance;
}
}分析:这种写法懒加载(lazy loading)很明显,但致命的缺点是在多线程环境下不能正常工作(“永远不要使用线程不安全的单例” – from Tencent Bugly)
写法(二):懒汉式(线程安全)
代码:
1
2
3
4
5
6
7
8
9
10
11
12public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() { // 通过加锁🔐解决多线程不安全问题
if(instatnce == null) {
instance = new Singleton();
}
return instance;
}
}分析:这种写法能够在多线程环境中很好地工作,而且看起来也具备很好的懒加载(lazy loading)能力。但是,遗憾的是,这种写法的效率很低(耗费内存),99% 的情况下不需要同步
写法(三):饿汉式(实例公有化)
代码:
1
2
3
4
5
6
7
8
9// Singleton with public final field
public class Singleton {
// 使用 public 修饰,因此无需 getInstance() 方法,可以直接拿到 instance 实例
// 此方法由 final 来保证每次返回的都是同一个对象的引用,私有的构造方法也只会被调用一次
public static final Singleton instance = new Singleton();
private Singleton() {}
}分析:优点是比较简洁(不再有优势)
写法(四):饿汉式(实例私有化)
代码:
1
2
3
4
5
6
7
8
9
10
11
12// Singleton with static factory
// 现代的 JVM 基本都内嵌了对 static factory 方法的调用,使得 public field 方式不再有优势
// 此方法更灵活,只需修改 getInstance() 的返回逻辑,而不需要改变 API 就可以将类改为非单例类
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}分析:这种写法基于 ClassLoader 机制从而避免了多线程环境下的同步问题。不过,instance 在类加载时就实例化(虽然导致类加载的原因有很多),在单例模式中大多都是因为调用 getInstance() 方法,但是也不能确定有其他的方式(或者其他的静态方法)导致类加载,这时候初始化 instance 显然没有达到懒加载(lazy loading)的效果
写法(五):饿汉式(静态代码块)
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Singleton {
private Singleton instance = null;
private Singleton() {}
static {
instance = new Singleton(); // 基静态、子静态 -> 基实例代码块、基构造 -> 子实例代码块、子构造
}
public static Singleton getInstance() {
return this.instance;
}
}分析:三种饿汉式差别不大,都是在类初始化时实例化 instance
写法(六):静态内部类(ClassLoader)
代码:
1
2
3
4
5
6
7
8
9
10
11
12public class Singleton {
private static class SingletonHolder { // 私有的静态内部类
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}分析:这种写法同样利用了 ClassLoader 的类加载机制来保证初始化 instance 时只有一个线程。它跟饿汉式写法不同的是(很细微的差别):饿汉式是只要 Singleton 类被加载了,那么 instance 就会被实例化(没有达到懒加载的效果),而静态内部类的写法是即使 Singleton 类被加载了,instance 却不一定被实例化。因为 SingletonHolder 内部类没有被主动使用过,只有显示通过调用 getInstance() 方法时,才会显示加载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,我想让它延迟加载;另一方面,我不希望在 Singleton 类加载时就实例化,因为我不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,静态内部类的写法相比饿汉式写法就显得很合理
写法(七):双重校验锁(DCL)
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Singleton {
// volatile 关键字的作用:保证内存可见性、保证有序性(禁止指令重排),但不保证原子性
// 大部分情况下不加 volatile 也是可以的,但如果发生指令重排,则会在第二次 check 处导致线程不安全
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null) { // 第一次 check,避免不必要的同步
synchronized(Singleton.class) { // 仅同步实例化的代码块
if(instance == null) { // 第二次 check,保证线程安全
instance = new Singleton();
}
}
}
return instance;
}
}
// TODO: 还有优化的空间,参见底部参考部分第二篇文章,可以算做第十一种写法分析:这种写法是第二种写法的升级版,不过在 JDK 1.5 之后,双重校验锁才能达到单例效果
写法(八):枚举
代码:
1
2
3
4
5
6
7
8public enum Singleton {
INSTANCE;
// 枚举同普通类一样,可以有自己的成员变量和方法
public void do() {
System.out.println("Do whatever you want");
}
}分析:这种写法是 《Effective Java》 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题(创建枚举默认就是线程安全的),而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒。不过,个人认为由于在 JDK 1.5 中才加入 enum 特性,用这种方式写不免让人感觉生疏。在实际工作中,也很少见有人这么写过
优点:
- 枚举单例写法简单:肉眼可见
- 枚举可解决线程安全问题:反序列化后,可以发现内部使用了 static、final 修饰每一个枚举项
- 枚举可避免反序列化破坏单例:普通类的反序列化会使用反射,反序列化后的对象是重新 new 出来的,这就破坏了单例,而枚举的序列化和反序列化是有特殊定制的
写法(九):CAS 方式
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class Singleton {
// AtomicReference 是原子引用类型,用来以原子方式更新复杂类型
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton> ();
private Singleton() {}
public static Singleton getInstance() {
for(;;) {
Singleton instance = INSTANCE.get();
if(instance != null) {
return instance;
}
instance = new Singleton();
// CAS 方法有两个参数 expect 和 update,以原子方式实现了比较并设置的功能
// 如果当前值等于 expect,则更新为 update 并返回 true;否则不更新并返回 false
if(INSTANCE.compareAndSet(null, instance)) {
return instance;
}
}
}
}分析:
- 上面几种线程安全的单例写法,虽然没有在代码中显示的使用 synchronized(除了双重校验锁写法),但是,都是借助了 ClassLoader 的线程安全机制。而 ClassLoader 的线程安全机制底层也是使用了 synchronized 的;ClassLoader 的 loadClass() 方法在加载类的时候使用了 synchronized 关键字。也正是因为这样, 除非被重写,这个方法默认在整个类加载过程中都是同步的(线程安全的)
- 优点:不需要使用传统的锁机制来保证线程安全,CAS 是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度
- 缺点:如果忙等待一直执行不成功(一直在死循环中),会对 CPU 造成较大的执行开销。而且,这种写法如果有多个线程同时执行
singleton = new Singleton();
也会比较耗费堆内存
写法(十):Lock 方式
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 类似双重校验锁写法
public class Singleton {
private static Singleton instance = null;
private static Lock lock = new ReentrantLock();
private Singleton() {}
public static Singleton getInstance() {
if(instance == null) {
lock.lock(); // 显式调用,手动加锁
if(instance == null) {
instance = new Singleton();
}
lock.unlock(); // 显式调用,手动解锁
}
return instance;
}
}参考:你写的单例是线程安全的吗
2. 对比
单例模式 | 是否推荐 | 懒加载 | 反序列化单例 | 反射单例 | 克隆单例 | 性能、失效问题 |
---|---|---|---|---|---|---|
(三种)饿汉模式 | eager 加载推荐 | ✘ | ✘ | ✘ | ✘ | 注意是急切初始化 |
(包括同步方法)懒汉模式 | ✘ | ✔️ | ✘ | ✘ | ✘ | 存在性能问题,每次获取实例都会同步 |
静态内部类 | 推荐 | ✔️ | ✘ | ✘ | ✘ | ✘ |
双重校验锁(DCL) | 可用 | ✔️ | ✘ | ✘ | ✘ | JDK < 1.5 失效 |
枚举 | 最推荐 | ✔️ | ✔️ | ✔️ | ✔️ | JDK < 1.5 不支持 |
3. 总结
有两种场景可能导致非单例的情况(还有一种克隆的场景):
场景一:如果单例由不同的类加载器加载,那便有可能存在多个单例类的实例。假设不是远端存取,例如一些 servlet 容器,对每个 servlet 使用完全不同的类加载器,这样的话如果有两个 servlet 访问一个单例类,它们就会有各自的实例
解决办法:
1
2
3
4
5
6
7
8
9private static Class getClass(String className) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader = null) {
classLoader = Singleton.class.getClassLoader();
}
return (classLoader.loadClass(className));
}
场景二:如果 Singleton 实现了
java.io.Serializable
接口,那么这个类的实例就可能被序列化和反序列化。不论如何,如果序列化一个单例类的对象,接下来反序列多个那个对象,那就会有多个单例类的实现解决办法:
1
2
3
4
5
6
7
8
9public class Singleton implements Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {}
private Object readResolve() {
return INSTANCE;
}
}
就个人来说,更倾向第三种(饿汉式)和第五种(静态内部类),简单易懂,而且在 JVM 层实现了线程安全(如果不是多个类加载器的情况)
- 一般情况下,会使用第三种(饿汉式),只有在要明确实现懒加载效果时才会使用第五种(静态内部类)
- 另外 ,如果涉及到反序列化创建对象时会尝试使用第六种(枚举)的方式来实现单例。不过,尽量保证我们程序是线程安全的,而且尽量不会使用懒汉式
- 如果有其他特殊需求,可能会使用第七种(双重校验锁)方式,毕竟,JDK 1.5 已经没有双重检查锁定的问题了
一般来说,第一种(懒汉式,线程不安全)不算单例,第三种(饿汉式,线程安全)和第四种(饿汉式,静态代码块变种)就是一种。如果算的话,第五种(静态内部类)也可以分开写了。所以,一般单例都是六种写法,即:懒汉式、饿汉式、静态内部类、枚举、双重校验锁、CAS 方式