概述
三种类型
设计模式分为三大类型,共计23种模式
-
创建型模式:单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
-
结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
-
行为型模式:模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式
六大原则
单一职责原则:不要让一个类承担过多的职责。避免职责耦合在一起,避免一个职责的变化影响到其他职责
开放封闭原则:需求改变时,我们应该尽量通过拓展的方式、即加入新代码来实现变化
里氏替换原则:只要父类出现的地方,子类就可以出现,替换为子类后不会产生任何错误和异常
依赖倒置原则:接口或抽象类不依赖于实现类,而实现类依赖接口或抽象类
迪米特原则:当其中某一个类发生修改时,就会尽量少地影响其他模块,降低系统的耦合度,使类与类之间保持松散的耦合关系
接口隔离原则:建立单一接口而不是建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少
单例
首先,为什么会存在单例这种模式,它的优势是什么?先在心中思考一下你熟悉的几种单例方式,我们都知道单例的几种实现方式,懒汉、饿汉、内部类、枚举等,这些实现方式各自有什么特点,适用场景是哪些呢?这篇文章会带着这些问题总结单例的各种实现方式
饿汉
饿汉模式,顾名思义,就是这位打工人对干饭特积极,不管饿不饿都跟饿死鬼似的,老早的就把干饭工具准备好了( new 对象),随时准备干饭。
静态变量
那么干饭的姿势也分为几种,首先上场的是直接定义“静态变量”选手:
public class SingletonStaticHungry {
/**
* 饿汉式:静态常量1
*/
public static final SingletonStaticHungry SINGLE_TON = new SingletonStaticHungry();
private SingletonStaticHungry() {
}
/**
* 饿汉式:静态常量2
*/
private static final SingletonStaticHungry SINGLE_TON_TWO = new SingletonStaticHungry();
public static SingletonStaticHungry getSingleTon() {
return SINGLE_TON_TWO;
}
}
“静态变量”主打一个朴实无华,写法简单易懂,但因为其写法简答粗暴,也有其局限性。
静态常量这种写法的优缺点很明显,
优点:这种写法比较简单,就是在类加载的时候就完成实例化。避免了线程同步问题。
缺点:在类加载的时候就完成实例化,没有达到Lazy Loading的效果。如果从未使用过这个实例,则会造成内存的浪费。
静态代码块
public class SingletonStaticHungry {
/**
* 饿汉式:静态代码块
*/
private static final SingletonStaticHungry SINGLE_TON_THREE;
// 在静态代码块执行时,创建单例对象
static {
SINGLE_TON_THREE = new SingletonStaticHungry();
}
public static SingletonStaticHungry getSingleTonThree() {
return SINGLE_TON_THREE;
}
}
这种方式是将类实例化的过程放在了静态代码块中,也是在类加载的时候,就执行静态代码块中的代码,初始化类的实例。
静态内部类
public class SingletonInnerClass {
private SingletonInnerClass() {
}
private static class SingletonInHolder {
private static final SingletonInnerClass SINGLETON_IN = new SingletonInnerClass();
}
public static SingletonInnerClass getSingletonIn() {
return SingletonInHolder.SINGLETON_IN;
}
}
这种方式跟饿汉式方式采用的机制类似,但又有不同,两者都是采用了类装载的机制来保证初始化实例时只有一个线程,在饿汉式方式是只要SingletonStaticHungry类被装载就会实例化;
而静态内部类的实现方式:
- 采用了类加载的机制来保证初始化实例时只有一个线程。
- 静态内部类方式在
SingletonInnerClass
类被加载时并不会立即实例化,而是在需要调用getSingletonIn
方法,才会加载SingletonInHolder
类,从而完成SingletonInnerClass
的实例化。 - 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们
保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
懒汉
懒汉的实现方式有很多种,有些是非线程安全的,并不推荐在项目中使用,如果遇到并发场景是可能会实例化多个对象,具体可参考下面的测试结果。
线程不安全
public class SingletonIdler {
private static volatile SingletonIdler singletonIdler;
public static SingletonIdler getInstance() {
// 这里线程是不安全的,可能得到两个不同的实例
if (singletonIdler == null) {
singletonIdler = new SingletonIdler();
}
return singletonIdler;
}
}
这种模式是线程不安全的,并不推荐使用。
问题:为什么不安全?
答案:2个线程进入此方法,其中一个线程进入if条件后,执行
singletonIdler =new SingletonIdler()
跳出时,
另一个线程也会执行singletonIdler = new SingletonIdler()
线程安全(但效率低)
public class SingletonIdler {
private static volatile SingletonIdler singletonIdler;
public static synchronized SingletonIdler getInstance2() {
if (singletonIdler == null){
singletonIdler = new SingletonIdler();
}
return singletonIdler;
}
}
这种模式虽然是线程安全的,但因为使用synchronized
锁整个方法,效率低,不推荐使用。
问题:这种方式明明是线程安全的,为什么也不推荐使用?
答案:效率太低了,每个线程在想获得类的实例时候,执行
getInstance2()
方法都要进行同步。
而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。
线程不安全方式2
看了上面的实现方式之后,不加锁不安全,加锁之后又说效率低,那么再优化一下,改加锁方式
public class SingletonIdler {
private static volatile SingletonIdler singletonIdler;
public static SingletonIdler getInstance3(){
if (singletonIdler == null){
synchronized (SingletonIdler.class){
singletonIdler = new SingletonIdler();
}
}
return singletonIdler;
}
}
测试一下并发结果(参考下面的测试数据),没想到依然是线程不安全的,虽然使用synchronized
。
问题:改了加锁方式之后,为什么又不安全了?
答案:虽然加了锁,但是等到第一个线程执行完
singletonIdler = new SingletonIdler()
跳出这个锁时,另一个进入if语句的线程同样会实例化另外一个Singleton对象,线程不安全的原理跟上面那个线程不安全的方法类似。
线程安全且效率高之双重校验锁
public class SingletonIdler {
private static volatile SingletonIdler singletonIdler;
public static SingletonIdler getInstance4(){
if (singletonIdler == null){
synchronized (SingletonIdler.class){
if (singletonIdler == null){
singletonIdler = new SingletonIdler();
}
}
}
return singletonIdler;
}
}
懒汉式变种,属于懒汉式的最好写法,保证延迟加载和线程安全,推荐的懒汉写法
枚举
借助枚举来实现单例模式,不仅能避免多线程同步问题,而
且还能防止反序列化重新创建新的对象
public enum SingletonEnum {
/**
* 枚举单例
*/
instance;
SingletonEnum() {
}
public void whateverMethod() {
System.out.println("枚举单例……");
}
}
单例测试
测试饿汉线程不安全模式
public class SingletonIdlerTest extends TestCase {
@Test
public void testGetInstance() throws InterruptedException {
//创建线程池,使用多线程形式获取SingletonIdler的实例对象
int taskCount = 10;
CountDownLatch countDownLatch = new CountDownLatch(taskCount);
ExecutorService executorService = new ThreadPoolExecutor(
10, // 核心线程数
100, // 最大线程数
0L, // 空闲线程存活时间
TimeUnit.MILLISECONDS, // 时间单位
new LinkedBlockingQueue<>(1000), // 工作队列
new ThreadFactory() { // 自定义线程工厂
private final AtomicInteger threadIndex = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("ThreadPoolTask-" + threadIndex.getAndIncrement());
thread.setDaemon(true); // 设置为守护线程(可选)
return thread;
}
});
// 使用线程池执行任务...
//创建一个线程安全的list
List<SingletonIdler> list = new CopyOnWriteArrayList<>();
Map<SingletonIdler, AtomicInteger> mapAtom = new ConcurrentHashMap<>();
for (int i = 0; i < taskCount; i++) {
executorService.execute(() -> {
//调用饿汉式单例的各个方法获取单例,替换
SingletonIdler singletonIdler = SingletonIdler.getInstance();
// SingletonIdler singletonIdler = SingletonIdler.getInstance2();
// SingletonIdler singletonIdler = SingletonIdler.getInstance3();
// SingletonIdler singletonIdler = SingletonIdler.getInstance4();
list.add(singletonIdler);
AtomicInteger atomicInteger = mapAtom.computeIfAbsent(singletonIdler, k -> new AtomicInteger(0));
atomicInteger.incrementAndGet();
mapAtom.put(singletonIdler, atomicInteger);
countDownLatch.countDown();
});
}
//等待所有任务执行完毕
countDownLatch.await();
//比较list中存在几种不同的元素,因为是单例模式,所以如果是线程安全的写法,应该存在10个相同的元素,比较方式就是拿出第一个元素和后面的元素进行比较
System.out.println("list中相同元素数量:" + Collections.frequency(list, list.get(0)));
System.out.println("map中存在几种不同的元素:" + mapAtom.size());
Assert.assertEquals(1, mapAtom.size());
}
}
我们直接创建junit测试类,写好注释,让通义千问ai插件帮我们写测试代码,执行以上代码可以看到,结果如下:
调用SingletonIdler.getInstance();
list中相同元素数量:1
map中存在几种不同的元素:3
java.lang.AssertionError:
Expected :1
Actual :3
调用SingletonIdler.getInstance2();
list中相同元素数量:10
map中存在几种不同的元素:1
我们直接debug断点看下单例对象有几种。如图所示:
调用SingletonIdler.getInstance3();
list中相同元素数量:1
map中存在几种不同的元素:2
java.lang.AssertionError:
Expected :1
Actual :2
调用SingletonIdler.getInstance4();
list中相同元素数量:10
map中存在几种不同的元素:1
可将以上代码的SingletonIdler替换成SingletonStaticHungry或者SingletonInnerClass即可测试饿汉或者静态内部类单例的线程安全,通过测试发现饿汉式的静态变量、静态代码块,静态内部类等几种实现方式实际结果与预期结果一致,相同元素数量为10,map中只存在一种元素
总结
从以上的代码中,我们可以看到单例设计模式的优势以及使用场景,优势:
-
单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需
要频繁创建销毁的对象,使用单例模式可以提高系统性能。 -
当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使
用new。
使用场景如下
- 数据库连接池:为了高效地管理和共享数据库连接资源,确保系统中只有一个全局的数据库连接池实例。
- 日志服务/日志处理器:为了避免重复的日志系统初始化和减少内存开销,通常会将日志服务实现为单例,确保所有模块都共用一个日志记录器实例。
- 系统配置管理器:应用程序通常需要加载和缓存配置信息,将其设计为单例可以保证在整个应用生命周期内全局配置的唯一性和一致性。
- 缓存类:如对象缓存、数据缓存等,这些类往往只需要一个实例来存储和检索数据,以避免重复创建缓存带来的性能损失。
- 工具类或者服务类:当某些工具类或服务类的实例只需且必须有一个时,例如线程池、上下文环境管理器等。
- 全局状态管理器或者状态机:如果系统中存在某种状态需要全局统一管理,单例模式就是一个合适的选择。
- 频繁访问的对象:对于那些被多个客户端频繁请求,但是其状态无需独立存在的对象,采用单例模式能够减少不必要的对象创建,提高系统效率。
总的来说,单例设计模式主要应用于那些需要控制全局唯一实例、减少资源消耗、以及协调系统行为的场景。
扩展
在写单元测试时,我想用map来存储并发环境下获取的单例对象以及他们的数量,初始版本为:
List<SingletonIdler> list = new CopyOnWriteArrayList<>();
Map<SingletonIdler, AtomicInteger> mapAtom = new ConcurrentHashMap<>();
for (int i = 0; i < taskCount; i++) {
executorService.execute(() -> {
//调用饿汉线程不安全的方法获取单例
SingletonIdler singletonIdler = SingletonIdler.getInstance();
list.add(singletonIdler);
//*****注意这行代码
AtomicInteger atomicInteger = mapAtom.getOrDefault(singletonIdler, new AtomicInteger(0));
atomicInteger.incrementAndGet();
mapAtom.put(singletonIdler, atomicInteger);
countDownLatch.countDown();
});
}
执行本方法后发现了问题,
方法执行后,mapAtom的value值相加数量不等于10,理论上10个线程获取10个单例对象,如果线程安全的单例,map的结果因为1个key,value为10,但实际上map的value相加小于10,而将
AtomicInteger atomicInteger = mapAtom.getOrDefault(singletonIdler, new AtomicInteger(0));
替换为AtomicInteger atomicInteger = mapAtom.computeIfAbsent(singletonIdler, k -> new AtomicInteger(0));
后mapAtom的value值相加数量等于10
如图所示:
为什么会产生这种现象呢?GPT解答如下
在原始代码中,使用了
mapAtom.getOrDefault(singletonIdler, new AtomicInteger(0))
来获取或创建一个与 singletonIdler 关联的 AtomicInteger。然而,当一个新的AtomicInteger(0)
被创建并返回时,并没有添加到 mapAtom 中。然后您调用incrementAndGet()
方法来增加计数,接着再将这个 AtomicInteger 放入 map 中。这样会导致多次任务执行过程中对同一个 SingletonInnerClass 实例的原子计数器被多次初始化为 0,因此最后相加的结果不等于10。
而在替换为AtomicInteger atomicInteger = mapAtom.computeIfAbsent(singletonIdler, k -> new AtomicInteger(0));
后,computeIfAbsent
方法会确保对于给定的键(这里是指 SingletonInnerClass 实例),如果它不存在于映射中,则将计算出的新值(这里是新的 AtomicInteger(0))放入映射中。这样一来,每个 SingletonInnerClass 实例只会对应一个唯一的、初始化为 0 的 AtomicInteger,所有任务对同一个实例的计数都会累加到这个共享的 AtomicInteger 上,因此最后相加的结果会等于10