设计模式之单例模式
所谓单例,就是整个程序有且仅有一个实例。该类负责创建自己的对象,同时确保只有一个对象被创建。在Java,一般常用在工具类的实现或创建对象需要消耗资源。
一、简述
特点
- 类构造器私有
- 持有自己类型的属性
- 对外提供获取实例的静态方法
单例模式的优点
1、单例模式内存中只有一个实例,减少内存开支,一个对象需要频繁的创建和销毁。使用单例模式具有很大的优势。
2、单例只生成一个实例,减少系统的性能开销。
3、单例模式可以在避免对资源的多重占用。
4、单例模式可以在系统设置全局的访问点,优化和共享资源访问。
单例模式的缺点
1、单例模式一般没有接口,扩展很困难。
2、单例对测试不利
3、单例与单一职责原则有冲突。
单例的使用场景
1、要求生成唯一序列号的环境
2、在整个项目中需要一个共享访问点或访问数据
3、创建一个对象需要消耗的资源过多
4、需要定义大量的静态常量和静态方法。
二、单例的五种实现
1、懒汉模式
线程不安全,延迟初始化,严格意义上不是不是单例模式
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
2、饿汉模式
线程安全,比较常用,但容易产生垃圾,因为一开始就初始化
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
3、双重锁模式
线程安全,延迟初始化。这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
singleton=new Singleton()
对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile
修饰signleton
实例变量有效,解决该问题。4、静态内部类单例模式
public class Singleton { private Singleton(){ } public static Singleton getInstance(){ return Inner.instance; } private static class Inner { private static final Singleton instance = new Singleton(); } }
5、枚举单例模式
默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。实际上
- 枚举类隐藏了私有的构造器。
- 枚举类的域 是相应类型的一个实例对象
那么枚举类型日常用例是这样子的:
public enum Singleton { INSTANCE //doSomething 该实例支持的行为 //可以省略此方法,通过Singleton.INSTANCE进行操作 public static Singleton get Instance() { return Singleton.INSTANCE; } }
在以上所有的单例模式中,推荐静态内部类单例模式。主要是非常直观,即保证线程安全又保证唯一性。
众所周知,单例模式是创建型模式,都会新建一个实例。那么一个重要的问题就是反序列化。当实例被写入到文件到反序列化成实例时,我们需要重写
readResolve
方法,以让实例唯一。private Object readResolve() throws ObjectStreamException{ return singleton; }
三、单例注册表
另外一种特殊化的单例模式,它被称为单例注册表。
Import java.util.HashMap; Public class RegSingleton{ Static private HashMap registry=new HashMap(); //静态块,在类被加载时自动执行 Static{ RegSingleton rs=new RegSingleton(); Registry.put(rs.getClass().getName(),rs); } //受保护的默认构造函数,如果为继承关系,则可以调用,克服了单例类不能为继承的缺点 Protected RegSingleton(){} //静态工厂方法,返回此类的唯一实例 public static RegSingleton getInstance(String name){ if(name==null){ name=” RegSingleton”; }if(registry.get(name)==null){ try{ registry.put(name,Class.forName(name).newInstance()); }Catch(Exception ex){ex.printStackTrace();} } Return (RegSingleton)registry.get(name); } }
下面我们来看看Spring中的单例实现,当我们试图从Spring容器中取得某个类的实例时,默认情况下,Spring会才用单例模式进行创建。
<bean id="date" class="java.util.Date"/> |
<bean id="date" class="java.util.Date" scope="singleton"/> |
<bean id="date" class="java.util.Date" singleton="true"/> |
以上三种创建对象的方式是完全相同的,容器都会向客户返回Date类的单例引用。那么如果我不想使用默认的单例模式,每次请求我都希望获得一个新的对象怎么办呢?很简单,将scope属性值设置为prototype(原型)就可以了。
<bean id="date" class="java.util.Date" scope="prototype"/>
通过以上配置信息,Spring就会每次给客户端返回一个新的对象实例。
那么Spring对单例的底层实现,到底是饿汉式单例还是懒汉式单例呢?呵呵,都不是。Spring框架对单例的支持是采用单例注册表的方式进行实现的,源码如下:
public abstract class AbstractBeanFactory implements ConfigurableBeanFactory{ /** * 充当了Bean实例的缓存,实现方式和单例注册表相同 */ private final Map singletonCache=new HashMap(); public Object getBean(String name)throws BeansException{ return getBean(name,null,null); } ... public Object getBean(String name,Class requiredType,Object[] args)throws BeansException{ //对传入的Bean name稍做处理,防止传入的Bean name名有非法字符(或则做转码) String beanName=transformedBeanName(name); Object bean=null; //手工检测单例注册表 Object sharedInstance=null; //使用了代码锁定同步块,原理和同步方法相似,但是这种写法效率更高 synchronized(this.singletonCache){ sharedInstance=this.singletonCache.get(beanName); } if(sharedInstance!=null){ ... //返回合适的缓存Bean实例 bean=getObjectForSharedInstance(name,sharedInstance); }else{ ... //取得Bean的定义 RootBeanDefinition mergedBeanDefinition=getMergedBeanDefinition(beanName,false); ... //根据Bean定义判断,此判断依据通常来自于组件配置文件的单例属性开关 //<bean id="date" class="java.util.Date" scope="singleton"/> //如果是单例,做如下处理 if(mergedBeanDefinition.isSingleton()){ synchronized(this.singletonCache){ //再次检测单例注册表 sharedInstance=this.singletonCache.get(beanName); if(sharedInstance==null){ ... try { //真正创建Bean实例 sharedInstance=createBean(beanName,mergedBeanDefinition,args); //向单例注册表注册Bean实例 addSingleton(beanName,sharedInstance); }catch (Exception ex) { ... }finally{ ... } } } bean=getObjectForSharedInstance(name,sharedInstance); } //如果是非单例,即prototpye,每次都要新创建一个Bean实例 //<bean id="date" class="java.util.Date" scope="prototype"/> else{ bean=createBean(beanName,mergedBeanDefinition,args); } } ... return bean; } }
Spring对bean实例的创建是采用单例注册表的方式进行实现的,而这个注册表的缓存是HashMap对象,如果配置文件中的配置信息不要求使用单例,Spring会采用新建实例的方式返回对象实例。
参考文章:
https://www.jianshu.com/p/3bfd916f2bb2
https://blog.csdn.net/cs408/article/details/48982085
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架