【设计模式】并发编程下的单例模式
一、饿汉单例#
1. 静态变量实现#
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
这种实现方式基于class loader机制避免了多线程的同步问题。不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance
方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
2. 静态代码块实现#
public class Singleton {
private Singleton instance = null;
private Singleton() {}
static {
instance = new Singleton();
}
public static Singleton getInstance() {
return this.instance;
}
}
这种实现方式表面上看起来与上面差别挺大的,其实跟上面的差别不大,都是在类初始化即实例化instance。
二、懒汉单例#
1. 基本实现,线程不安全#
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种是最基本的lazy loading实现的单例模式,但是缺点很明显,在多线程下显然不能安全工作。
2. synchronized同步静态方法,线程安全#
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法能够很好的在多线程中工作,不会出现并发安全问题,也实现了懒加载。但是效率很低,synchronized使得这里并行变成串行。所以这种写法一般不会被使用到。
3. 静态内部类实现,推荐使用#
public class Singleton {
// === 静态内部类 ===
private static class SingletonHolder {
private static final INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种方式同样利用了class loader 的机制来保证初始化instance时只有一个线程,它跟上面提到的饿汉单例不同的是(很细微的差别):上面的饿汉单例是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy-loading效果)。而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder
类没有被主动使用,只有显示调用getInstance()
方法时,才会显示装载SingletonHolder
类,从而实例化instance。
4. 双重校验锁实现,推荐使用#
public class Singleton {
// ===1:volatile修饰
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
// ===2:减少不必要同步,性能优化
if (singleton == null) {
// ===3:同步,线程安全
synchronized(Singleton.class) {
if (singleton == null) {
// ===4:创建singleton对象
singleton = new Singleton();
}
}
}
return singleton;
}
}
-
为什么要使用volatile修饰?
虽然已经使用synchronized进行同步,但是第四步创建对象时,会有下面的伪代码:
memory=allocate(); // 1. 分配内存空间 ctorInstance(); // 2. 初始化对象 singleton=memory; // 3. 设置singleton指向刚排序的内存空间
当线程A在执行上面伪代码时,2和3可能会发生重排序,因为重排序并不影响运行结果,还可以提升性能,所以 JVM 是允许的。如果此时伪代码发生重排序,步骤为1 -> 3 -> 2,线程A执行到第3步时,线程B调用
getInstance()
方法,在判断singleton == null
时不为null
,则返回singleton
。但此时singleton
并没有初始化完毕,线程B访问的将是个还没初始化完毕的对象。当声明对象的引用为volatile后,伪代码的2、3的重排序在多线程中将被禁止!
5. 枚举实现,推荐使用#
public class Singleton {
private Singleton(){}
// === 延迟加载
private enum EnumHolder {
INSTANCE;
private static Singleton instance = null;
private Singleton getInstance() {
instance = new Singleton();
return instance;
}
}
public static Singleton getInstance() {
return EnumHolder.INSTANCE.instance;
}
}
三、问题注意#
1. 不同类加载器加载#
如果单例由不同的类加载器加载,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些Servlet容器对每个Servlet使用完全不同的类加载器,这样的话如果有两个Servlet访问一个单例类,它们就都会有各自的实例。
可以用下面方式对这个问题进行修复:
private static Class getClass(String classname) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null) {
classLoader = Singleton.class.getClassLoader();
}
return (classLoader.loadClass(classname));
}
2. 序列化问题#
如果Singleton实现了java.io.Serializable
接口,那么这个类的实例就可能被序列化和复原,不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {}
private Object readResolve() {
return INSTANCE;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义