ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

[设计模式]单例模式

2022-08-29 00:01:02  阅读:216  来源: 互联网

标签:IdGenerator private instance 模式 单例 SQL 设计模式 public


 

单例模式

本章笔记的内容主要参考《设计模式之美》

核心问题

<aside> ❓ 1.为什么要使用单例? 2.单例存在的问题? 3.单例与静态类的区别? 4.替代方案?

</aside>

为什么要使用单例模式

/在很多场景中,我们需要一些可以共享的对象,来统一操作一些资源。若此时,产生了多个实例,则这些原本应该共享的资源,会产生冲突或覆盖的现象。

举个例子,比如日志记录类。一般来说,日志纪录类会像固定的文件中输出日志结果,此时若使用多个实例进行这一操作,对于文件内容的write操作可能会出现覆盖的现象。当然,这种情况下可以使用类级的锁来保证正确性,但相比而言,单例是一种更节约资源的做法。另外,在业务系统中,涉及到如配置、唯一ID生成器这样的需求,一般也会使用单例模式。实际上,在Spring中管理的Bean对象都是基于单例模式的。

几种实现单例模式的方式

1.饿汉模式

public class IdGenerator {
	private AtomicLong id = new AtomicLong(0);
	private static final IdGenerator instance = new IdGenerator();
	private IdGenerator() {}
	public static IdGenerator getInstance() {
		return instance;
	}
	public long getId() {
		return id.incrementAndGet();
	}
}

2.懒汉模式

基础版本

public class IdGenerator {
	private AtomicLong id = new AtomicLong(0);
	private static IdGenerator instance;
	private IdGenerator() {}
	public static synchronized IdGenerator getInstance() {
		if (instance == null) {
		instance = new IdGenerator();
		}
		return instance;
	}
	public long getId() {
		return id.incrementAndGet();
	}
}

基础版本中使用了对方法加锁的方式,会极大的影响性能,不支持高并发。

双重检测

public class IdGenerator {
	private AtomicLong id = new AtomicLong(0);
	private static IdGenerator instance;
	private IdGenerator() {}
	public static synchronized IdGenerator getInstance() {
		if (instance == null) {
		instance = new IdGenerator();
		}
		return instance;
	}
	public long getId() {
		return id.incrementAndGet();
	}
}

实际上大多数的实现中,还会给单例类上声明volatile关键字,来避免new动作非原子性导致的问题。具体的可以参考:

The "Double-Checked Locking is Broken" Declaration

实际上这个问题在高版本的JDK中已经做了相应的原子化处理,即使不使用volatile,也能保证正确性。

静态内部类

public class IdGenerator {
	private AtomicLong id = new AtomicLong(0);
	private IdGenerator() {}
	private static class SingletonHolder{
		private static final IdGenerator instance = new IdGenerator();
	}
	public static IdGenerator getInstance() {
		return SingletonHolder.instance;
	}
	public long getId() {
		return id.incrementAndGet();
	}
}

使用了一种简单的方式,达到了懒加载和线程安全的目的。线程安全由JVM在初始化静态内部类时保证。

枚举

还可以使用枚举的方式来实现单例,在此不再赘述。

单例存在的问题

由于单例隐藏了初始化的细节,因此在初始化时,往往使用了硬编码的方式,这其实是一种反模式。在后续维护中,若业务需求发生了变化,相应的逻辑变更会相对困难。这里引用一个《设计模式之美》中的例子:

在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据 库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统 中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资 源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔 离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独 享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到 其他 SQL 的执行。

另外,单例模式的可测试性不高这也是因为代码中存在较多的硬编码,导致一些输入难以mock测试

单例的语义

在讨论单例模式时,我们会强调其唯一性。但在不同的前提条件下,唯一性的语义是不同的,在默认的语境中,单例模式指的是在同一个进程中,一个类仅有一个对象。当然这个前提条件可能会因为业务落地的实际场景发生变化。

线程中的唯一性

我们如何实现一个类在一个线程中的唯一性?实际上可以直接使用Java中的ThreadLocal类帮助我们实现,或者我们也可以自己在类中定义一个静态的ConcurrentHashMap,并使用线程id为Key,不同的实例为Value进行实现。

分布式环境下的唯一性

那么,在分布式多节点的环境下,如何保证实例的唯一性呢?通常的做法是,使用分布式文件系统,创建一个多节点共享的文件(该实例的本质是唯一的文件)。我们在创建、修改、读取实例时,我们总是从文件中反序列化得到实例,然后进行操作,最后将实例重新序列化回文件。这样就可以在不同的节点上,保证实例的唯一性。

参考文献

1.《设计模式之美》
2.双重检测
3.Reality Check, Douglas C. Schmidt, C++ Report, SIGS, Vol. 8, No. 3, March 1996.

标签:IdGenerator,private,instance,模式,单例,SQL,设计模式,public
来源: https://www.cnblogs.com/xy1997/p/16634509.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有