单例模式(Java + 实际案例)

关注微信公众号:程序驴,获取更多学习资料

何为单例模式

简单说,单例设计模式下的类在全局只能存在一个实例化对象,全局共同访问同一个实例。

老规矩,举个例子

lizi

单例模式通常是为了避免重复创建对象导致资源浪费。假如现在我有一台打印机,那我的舍友同学想打印的时候,就可以直接和我联系,我来提供打印机为他们打印,而不是每个想打印的同学都要买一台属于自己的打印机。这样就是没有利用现有的资源,明明利用一台打印机就能解决的问题,现在白白创建了很多台,这就是资源的浪费。

代码实现

单例模式的使用场景十分简单,如果某个类的实例化对象在全局只需要有一个,如果存在多个会导致资源浪费,或者其本身逻辑就不允许存在多个实例(比如全局计时器),这就需要将这种类改造成单例模式。

具体案例就用我们之前在适配器模式中展示的StorageConfig为例

单例模式的几种实现形式

懒汉式

懒汉式很容易理解,它比较懒,只有在别人调用它时,他才会创建实例化对象,并在之后一直沿用这个对象,不会重复创建。

image-20240418202737945

可以看到,懒汉式在返回实例化对象前会判断对象是否为空,如果为空说明是第一次访问,则需要创建对象,相反,如果不为空,则直接返回已经存在的对象,防止重复创建。

懒汉式优点:

  • 实现起来比较简单
  • 在需要实例化对象的时候才会创建(懒加载),避免空占系统资源

缺点:

懒汉式有非常明显的缺点:存在线程安全问题

具体存在线程安全的地方在判断实例化对象是否为空这一if语句上,假如现在有两个线程并发地请求刚初始化的Config类,它们都想要获取其中的alstorageAdapter,现在问题来了,两个线程同时到达了if判断,由于是刚初始化的Config类,其中的实例化对象肯定是为null,这样两线程就同时进入if内部并都创建了一遍alstorageAdapter,这显然不符合我们单例模式的要求。

饿汉式

饿汉式顾名思义,它比较饥渴😆,在代码中的体现是:当设计成懒汉式的类在被创建时,他就会在类的内部创建出实例化对象,并等待着别人来调用它的对象,给人一种迫不及待想要被访问的感觉,因此被称为饿汉式。

image-20240418201841686

此处就是利用饿汉式实现的适配器,可以看出饿汉式的实例在类中早已初始化好了,只需要在别人调用特定方法时返回已经创建好的对象即可。

饿汉式的好处:

  • 不会产生线程安全问题,因为对象早创建好了

缺点:

  • 存在浪费系统资源现象(无论会不会用到,都会在类中创建实例化对象)

针对线程安全做出的改造:使⽤类的内部类的懒汉模式

image-20240418205134315

这种方式利用了 classloader 机制来保证初始化 instance 时只有一个线程,这样做即能保证懒加载(只有访问OssService才会加载INSTANCE),且是线程安全的。

二重校验锁

image-20240418205821149

这种实现形式也是用的比较多的一种,他同样可以实现懒加载,同时因为在取得锁之后进行了第二重判断,避免了多线程重复创建的问题。

为什么?

我们先要了解一下如果直接加锁会带来的问题:

public static StorageAdapter getStorageAdapter() {
    if (alstorageAdapter == null) {
        synchronized (StorageConfig.class) {
            alstorageAdapter = new AliYunServiceAdapter(); // 直接创建对象,没有二次检查
        }
    }
    return alstorageAdapter;
}

多个线程同时首次调用getStorageAdapter方法时,尽管只有一个线程能够成功创建实例,但其余线程仍会进入同步块并执行new AliYunServiceAdapter语句(尽管此时alstorageAdapter已被创建)。这些线程经历了不必要的同步开销。

而二重校验:

public static StorageAdapter getStorageAdapter() {
    if (alstorageAdapter == null) { // 一重判定,是否要获取锁
        synchronized (StorageConfig.class) {
            if (alstorageAdapter == null) { // 二重判定,是否已经有其他线程创建了实例化对象
                alstorageAdapter = new AliYunServiceAdapter(); // 创建对象
            }
        }
    }
    return alstorageAdapter;
}

通过在同步块内部进行第二次检查,只有当alstorageAdapter确实未被创建时,线程才会执行实例化操作。这避免了多个线程在同步块内执行无意义的赋值操作,减少了资源浪费和提高了并发性能。

由单例模式二重校验想到的:

假设现在有这么一个场景,当你在项目中使用缓存时,必然会面临缓存的过期重建问题,如果过期的缓存是热点缓存(hot key),那么可能会有多个请求一起访问,并发现缓存已过期,这时想重建缓存就需要获取锁,只有获取到锁的线程才能执行更新缓存指令。这就和我们的单例模式有些类似:如果现在多个请求都在等待获取锁,而获取到锁的线程已经更新完数据到缓存中,那当它释放锁之后,必然会有另一个线程得到锁并再次更新缓存,这时如果也加入二次判断(判断缓存中是否存在数据),那岂不也同样避免了重复的查询操作,这其中节省下的性能开销还是比较可观的。

总结

所有设计模式源码已上传至gitee,地址:https://gitee.com/linfeng-show/design_pattern_-master

  • 18
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值