单例模式
- 单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
为什么要使用单例模式?
在某些业务情况下,我们需要保证某个类的全局唯一性
- 打印日志类。如果全局不唯一,日志文件成为竞争资源。可能被多个Looger类重写覆盖。就算是追加写,日志也会出现数据不一致的现象。
- 程序配置类信息,我们大多数情况希望程序中只存在一份。
- 全局ID生成器
如何实现一个单例模式?
- 延迟加载 在用到的时候再去加载。
- 创建过程的线程安全 表示可能会被其他线程通过
new
方式创建出来。 - fail-fast原则 是有问题及早暴露。避免程序在运行中暴露错误。这样有利于提高系统的可用性。
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();
}
}
- 在类加载的时候,就会加载
instance
静态实例。 - 优点:线程安全。并发度高。
- 缺点:不支持延迟加载。
2. 懒汉式
x
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();
}
}
- 为了保证单例创建的线程安全。我们给
getInstance
方法加了一大把锁。 - 优点:支持延迟加载。
- 缺点:并发度低。如果量化一下并发度只有
1
。性能下降。
3.双重检查
xxxxxxxxxx
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {}
//双重检查
public static IdGenerator getInstance() {
if (synchronized == null) {
synchronized(IdGenerator.class) { // 此处为类级别的锁
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
- 可以看见。我们只是将创建单例类的代码块使用
synchronized
锁起来了。在外一层提前判断了synchronized
是不是被实例化。这样做的好处就是 被枷锁的代码只会执行一次保证了线程安全。且不会被多个线程竞争。保证了并发度。 - 优点:并发度高。支持延迟加载。
- 缺点:无
4.静态内部类
xxxxxxxxxx
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();
}
}
- 在加载
IdGenerator
的时候,不会直接去实例化instance
。而是调用getInstance
方法时,才会去实例化。instance
的唯一性,创建过程的线程安全性,都由 JVM 来保证。 - 优点:并发度高。支持延迟加载。相较于双重检查更简单。
- 缺点:无
5.静态内部类
xxxxxxxxxx
public enum IdGenerator {
INSTANCE;
//静态内部类
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
- 利用java枚举本身的 特性。保证实例的线程安全性和实例唯一性。
- 优点:并发度高。实现最简单。
- 缺点:不支持延迟加载。
单例模式存在哪些问题?
- 大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象,直接通过类似
IdGenerator.getInstance().getId()
这样的方法来调用就可以了。但是,这种使用方法有点类似硬编码(hard code),会带来诸多问题
1.对OOP特性的支持不友好
- 我们知道。OPP的四大特性是封装,抽象,继承,多态。**单例对抽象,继承,多态都支持得不好。
- 单例模式违背了基于接口而非实现得设计原则,也违背了广义上得OPP抽象特性。
2.单例会隐藏类之间得依赖关系
- 通过构造函数、参数传递等方式声明的类之间的依赖关系,我们通过查看函数的定义,就能很容易识别出来。
- 单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。
3.单例对代码的扩展性不友好
- 如果在系统设计初期。没有考虑到一些业务的拓展。
- 例如,在业务初期,我们会觉得系统中只会存在一份数据库连接池配置信息。但是随着业务的发展,有些sql执行非常缓慢。这样他就会阻塞其他sql的执行。那么我们就需要将快慢sql分开执行。所以就需要两份数据库连接池信息。
4.单例 对代码的可测试性不友好
- 单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。
5. 单例不支持有参数的构造函数
单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。
- 第一种解决思路就是:创建完实例之后,调用init()函数传递参数。需要注意的是,我们在使用这个单例的类的时候,要先调用init()方法。
- 第二种解决思路是:将参数放到
getIntance
方法中。但是这个方法有些问题,因为是单例的,所以在第二次设置的时候,就不会生效。 - 第三种解决思路是:将参数放到另一个全局变量中。里面的值既可以直接定义 ,也可以通过配置文件加载得到。
6.有何替代解决方案
- 为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。不过,静态方法这种实现思路,并不能解决我们之前提到的问题。如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,由程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。
如何设计实现一个集群环境下的分布式单例模式?
1.如何理解单例模式中的唯一性?
- 定义:一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
- 范围:集群唯一 > 线程唯一 > 进程唯一
- 小范围在大范围下不一定唯一。大范围在小范围下肯定唯一。
2.如何实现线程唯一的实例?
- 在代码中,我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。
3.如何实现集群环境下的单例
- ,我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
- 简化来说就是要找一个他们竞争资源来共享信息。
4.如何实现一个多例模式?
- 多利是一个对象可以有多个实例。
- 多利模式和工厂模式的区别在于工厂模式可以产生不同类的实例。而多利模式只能产生一个类的实例。
- 实现的话使用一个HashMap来存储
打赏
支付宝扫一扫
微信扫一扫