设计模式之单例模式
概述
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式提供一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。又名单件模式或单态模式。
UML图
实现
懒汉式
public class Singleton {
private static Singleton instance;
private Singleton () {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
所谓懒汉式,就是在类初始化时并没有实例化,需要时才去实例化。
上面的写法是线程不安全的。解决方法:在方法上增加synchronized关键词。
饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton () {}
public static Singleton getInstance() {
return instance;
}
}
基于classloder机制,instance在类装载时就实例化。
饿汉变形
public class Singleton {
private Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return this.instance;
}
}
枚举
public enum Singleton {
INSTANCE;
public void getInstance() {
}
}
编译后,可发现使用static final
静态内部类
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton () {
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
是懒汉式,线程安全的写法。
DCL
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance(){
if (singleton == null) {
synchronized(Singleton.class) {
if (singleton == null)
singleton = new Singleton();
}
}
return singleton;
}
}
应用
JDK
Runtime.getRuntime是单例模式。
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
MyBatis
MyBatis单例模式举例,ErrorContext和LogFactory,ErrorContext是用在每个线程范围内的单例,用于记录该线程的执行环境错误信息,而LogFactory则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。
public class ErrorContext {
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
private ErrorContext() {
}
public static ErrorContext instance() {
ErrorContext context = LOCAL.get();
if (context == null) {
context = new ErrorContext();
LOCAL.set(context);
}
return context;
}
}
构造函数是private修饰,具有一个static的局部instance变量和一个获取instance变量的方法,在获取实例的方法中,先判断是否为空如果是的话就先创建,然后返回构造好的对象。
LOCAL的静态实例变量使用ThreadLocal修饰,也就是说它属于每个线程各自的数据,而在instance()方法中,先获取本线程的该实例,如果没有就创建该线程独有的ErrorContext。
问题
static
一个类中所有的方法和属性都被标注为static,是单例吗?
是的,不过有线程安全问题。
性能
哪种写法性能最好?
安全
单例模式不是绝对安全,可以被破坏,即可以拿到不相等的两个实例。
反射
考虑如下代码:
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getSingleton();
try {
Class<Singleton> singleClass = (Class<Singleton>)Class.forName("aa.bb.Singleton");
Constructor<Singleton> constructor = singleClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
Singleton singletonByReflect = constructor.newInstance();
System.out.println("singleton : " + singleton);
System.out.println("singletonByReflect : " + singletonByReflect);
System.out.println("singleton == singletonByReflect : " + (singleton == singletonByReflect));
} catch (Exception e) {
}
}
}
输出:
singleton : aa.bb.Singleton@55d56113
singletonByReflect : aa.bb.Singleton@148080bb
singleton == singletonByReflect : false
结论:通过发射的方式即可获取到一个新的单例对象,单例模式被破坏。
解决:在Singleton的构造函数中增加判断
private Singleton() {
if (singleton != null) {
throw new RuntimeException("Singleton constructor called.");
}
}
反序列化
先将单例对象序列化后保存到临时文件中,然后再从临时文件中反序列化出来:
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getSingleton();
// Write Obj to file
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(singleton);
// Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton singletonBySerialize = (Singleton)ois.readObject();
System.out.println("singleton : " + singleton);
System.out.println("singletonBySerialize : " + singletonBySerialize);
System.out.println("singleton == singletonBySerialize : " + (singleton == singletonBySerialize));
} catch (Exception e) {
}
}
}
输出:
singleton : aa.bb.Singleton@617faa95
singletonBySerialize : aa.bb.Singleton@5d76b067
singleton == singletonBySerialize : false
解决:
在Sinleton中增加readResolve()方法,指定要返回的对象的生成策略即可:
private Object readResolve() {
return getSingleton();
}
在反序列化过程中,在反序列化执行过程中会执行到ObjectInputStream#readOrdinaryObject方法,这个方法会判断对象是否包含readResolve方法,如果包含的话会直接调用这个方法获得对象实例:
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
没有readResolve方法,则反序列化时通过反射创建对象。
Spring
Spring提供5种scope:singleton、prototype、request、session、global session,参考文档。其中常用的是前两者,默认是单例。
单例bean与原型bean的区别
如果一个bean被声明为单例的,在处理多次请求的时候在Spring容器里只实例化出一个bean,后续的请求都共用这个对象,这个对象会保存在一个map里面。当有请求来的时候会先从缓存map里查看有没有,有的话直接使用这个对象,没有的话才实例化一个新的对象,即单例模式。但是对于原型(prototype)bean来说当每次请求来的时候直接实例化新的bean,没有缓存以及从缓存查的过程。这个实现过程的源码在AbstractBeanFactory.doGetBean()
方法里面,源码挺长,略。
单例bean的优势
- 减少新生成实例的消耗
新生成实例消耗包括两方面,第一,spring会通过反射或者cglib来生成bean实例这都是耗性能的操作,其次给对象分配内存也会涉及复杂算法。 - 减少JVM垃圾回收
- 可以快速获取到bean
因为单例的获取bean操作除了第一次生成之外其余的都是从缓存里获取的所以很快。
单例bean的劣势
不能保证线程安全!由于所有请求都共享一个bean实例,有状态bean在并发下会出现问题;原型bean则不会有这样问题(例外,如被单例bean依赖),因每个请求都新创建实例。
另外,Spring对单例模式的实现,仅保证提供一个全局的访问点,即BeanFactory。但没有从构造器级别去控制单例的唯一性,因为Spring管理的是任意的Java对象。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix