本文是对于 Java 单例对象的整理文档,内容并非原创,站在巨人肩上看更远。
单例 Singleton,即只有一个实例。
一、饿汉模式
饿汉模式即为对象创建、初始化时 立即加载
单例对象。由于在运行时(除初始化外)不存在单例对象引用的变更,故线程安全。
饿汉式加载单例对象:
public class SingletonImmediately {
private static final SingletonImmediately INSTANCE = new SingletonImmediately();
public static SingletonImmediately getInstance() {
return INSTANCE;
}
private SingletonImmediately() {
}
}
复制代码
当然,也可以用 static 代码块进行实例化。
public class SingletonImmediately {
private static final SingletonImmediately INSTANCE;
static {
INSTANCE = new SingletonImmediately();
}
public static SingletonImmediately getInstance() {
return INSTANCE;
}
private SingletonImmediately() {
}
}
复制代码
关键点:
二、懒汉模式
懒汉模式,重在 懒
字,即需要用的时候才去加载单例对象(第一次使用时加载,如对象已存在无需加载),也唤为 “延迟加载”。
随手写出懒加载单例模式代码:
public class SingletonLazy {
private static SingletonLazy INSTANCE;
public static SingletonLazy getInstance() {
if (null == INSTANCE) {
// 第一次访问,实例化对象
INSTANCE = new SingletonLazy();
}
return INSTANCE;
}
private SingletonLazy() {
}
}
复制代码
很显然,这段代码漏洞百出,比如,如果多个线程同时执行, INSTANCE 则会实例化多次,大致流程如下:
问题就在这,多个线程并发访问导致数据未同步。那,加上同步吧。
public class SingletonLazy {
private static SingletonLazy INSTANCE;
public synchronized static SingletonLazy getInstance() {
if (null == INSTANCE) {
// 第一次访问,实例化对象
INSTANCE = new SingletonLazy();
}
return INSTANCE;
}
private SingletonLazy() {
}
}
复制代码
注意 getInstance 方法,添加了 synchronized 关键字修饰。
synchronized 关键字修饰 static 方法时,为类锁,即锁住的是整个 SingletonLazy 类。
很显然,这样的实现方式极为低效,每次访问 getInstance 都要拿 "类锁",实际上只需要第一次访问时获取锁,并进行对象的实例化即可。
public class SingletonLazy {
private static SingletonLazy INSTANCE;
public static SingletonLazy getInstance() {
if (null == INSTANCE) {
synchronized (SingletonLazy.class) {
INSTANCE = new SingletonLazy();
}
}
return INSTANCE;
}
private SingletonLazy() {
}
}
复制代码
似乎现在看起来,有模有样。细心的读者可能已经发现,还有优化空间。先分析下执行流程:
线程 A 进入方法体,instance 为 null,拿锁,开始实例化
线程 B 进入方法体,instance 为 null(此时线程 A 还没实例化完成),尝试拿锁,没拿到锁,阻塞等待
线程 A 实例化完成,出临界区,释放锁(注意,此处 instance != null)
由于线程 A 释放了锁,线程 B 拿到锁,开始实例化【额,实际上线程 A 已经实例化完成了】
OK,改造一下。拿到锁后,check 下不就好了。
public class SingletonLazy {
private static SingletonLazy INSTANCE;
public static SingletonLazy getInstance() {
if (null == INSTANCE) {
synchronized (SingletonLazy.class) {
// double check,防止在拿锁过程中,其他线程已经实例化完成,重复工作
if (null == INSTANCE) {
INSTANCE = new SingletonLazy();
}
}
}
return INSTANCE;
}
private SingletonLazy() {
}
}
复制代码
似乎,这样就完美了。答案当然是 no。
如果对于 Java 内存模型有过了解的同学,可能会认识到事情远没有这么简单。且听我分析一波。
分析之前,有个知识点得先同步下:对于 new 一个对象,实际上执行路径是
1. 分配一块内存 M
2. 将 M 的地址赋值给 INSTANCE 变量
3. 在内存 M 上初始化 SingletonLazy 对象
那么 getInstance 在多线程下的工作流程即为:
线程 A 进入 getInstance,instance == null,拿锁,开始实例化
线程 A 分配一个内存 M,并把 M 的地址赋值给 instance
此时,线程 B 进入 getInstance,此时 instance != null,直接返回【额,这里返回的 instance 实际上尚未实例化完成】
线程 B 使用 instance 【尴尬了,大概率直接 NPE】
线程 A 在内存 M 上初始化 SingletonLazy 对象
想必已经了解问题的根因,实际上这是一个并发中比较经典的,可见性 问题。小问题,volatile 解决它。
public class SingletonLazy {
private static volatile SingletonLazy INSTANCE;
public static SingletonLazy getInstance() {
if (null == INSTANCE) {
synchronized (SingletonLazy.class) {
// double check,防止在拿锁过程中,其他线程已经实例化完成,重复工作
if (null == INSTANCE) {
INSTANCE = new SingletonLazy();
}
}
}
return INSTANCE;
}
private SingletonLazy() {
}
}
复制代码
三、静态内部类实现单例模式
静态内部类也能实现线程安全的单例模式,实际上和 饿汉模式 区别不大。
public class SingletonStaticInnerClass {
public static Singleton getInstance() {
return Singleton.INSTANCE;
}
private static class Singleton {
private static Singleton INSTANCE = new Singleton();
}
private SingletonStaticInnerClass() {
}
}
复制代码
同样的,在序列化对象时,这段代码得到的对象也是多实例的。
解决办法就是在反序列化中使用 readResolve 方法
public class SingletonStaticInnerClass implements Serializable {
public static Singleton getInstance() {
return Singleton.INSTANCE;
}
private static class Singleton {
private static Singleton INSTANCE = new Singleton();
}
private SingletonStaticInnerClass() {
}
protected Object readResolve() throws ObjectStreamException {
return Singleton.INSTANCE;
}
}
复制代码
四、枚举类实现单例模式
枚举类实现单例对象实际上是比较优雅的,除了其能很简洁的实现外,利用枚举类的特性(常量池),还能保证序列化/反序列化后也是单例。
public enum SingletonEnumClass {
// 单例对象
INSTANCE
;
private Singleton instance;
public Singleton getInstance() {
return instance;
}
SingletonEnumClass() {
// 初始化逻辑
this.instance = new Singleton();
}
// 单例类
public class Singleton {
}
}
复制代码
可进一步优化下,职责更加单一:
public class SingletonEnumClass {
// 对外访问接口
public static Singleton getInstance() {
return SingletonEnum.INSTANCE.instance;
}
// 单例类
public static class Singleton {
}
// 枚举类,用于实例化单例对象
public enum SingletonEnum {
// 单例对象
INSTANCE;
private Singleton instance;
SingletonEnum() {
// 初始化逻辑
this.instance = new Singleton();
}
}
}
复制代码
参考文献
《Java 多线程编程核心技术》
《Java 并发编程之美》
评论