面试必考题之单例模式---懒汉模式加饿汉模式

单例模式是一种设计模式

第一句话,什么意思。

要知道什么是单例模式,首先要知道什么是设计模式。

之前学习过框架(比如spring这种,就是轻量级框架)

框架算是硬性的规定。

设计模式算是软性的规定。

怎么解释这个“软性的规定”呢。

就类似与,棋谱,对弈的时候,照着棋谱走,棋力就不会很差。再差也差不到哪里去。但是也不是说必须每一步都按照棋谱走,就算有一两步不是按照棋谱来走的,也不见得一定会输。

总之,框架设计模式都是大佬设计出来的,就算是我这种小菜鸡,也能根据设计模式写出不错的代码~~~。

这里补充一个误区,很多人都以为设计模式有23种,实际上,设计模式有很多种,不只有23种!!!

有3个大佬,合伙写了一本书,介绍了23种常用的设计模式~~~

这本书很火~~~导致有的人误以为,设计模式就这23种!!!

设计模式有很多种,不同的语言中,也有不同的设计模式。

(设计模式也可以认为是对编程语言语法的补充)

(一般本科阶段校招,掌握两个设计模式----一个单例模式,一个工厂模式,应对校招,完全够了)。

什么是单例?

单例 = 单个实例

实例就是对象,对象就是实例(这里的对象和java面向对象说的不是同一个,指的是一个程序,一个进程,一个主机)

如果一个类,在一个进程中,只能有一个实例(原则上不应该有多个)

使用单例模式,就能对咱们得代码进行一个更严格的校验和检查

有的代码需要使用一个对象,来管理/持有大量的数据,此时有一个对象就可以了。

比如,一个对象管理了10个G的数据。如果你不小心创建出多个对象,内存空间就会成倍增长

那么

唯一的对象如何保证?

当然可以通过“君子约定”的方式,写一个文档,文档上约定,每个接手代码的程序员,都不能把这个类创建多个实例~~~

(但是很明显,这个约定不靠谱,人是靠不住的,人总会出差错)

还是期望机器(编译器)能够对代码中指定的类,对创建的实例进行个数校验。如果发现创建多个实例了,就直接报错,编译无法通过这种。

如果能做到这样,就可以非常放心的编写代码,不必担心因为失误创建出多个实例了。

Java语法,本身没有办法直接约定某个对象最多只能创建几个实例!!!

这个时候就需要通过一些“奇技淫巧”来变相的实现这样的效果

实现单例模式的方式

实现单例模式的方式,有很多种,此处介绍两种最简单的实现方式

1. 懒汉模式

2. 饿汉模式

在软件设计中,饿汉模式和懒汉模式是两种常见的资源加载策略。

饿汉模式

这时候代码先创建一个类 Singleton

//期望这个类只能有唯一的实例(一个进程中)
    class Singleton {

    }

singleton这个单词源于single,single什么意思呢,什么不认识?

single dog你总认识了吧

说的就是你,屏幕前的单身狗☞

single dog是单手狗的意思,那么single自然就是单身,单个的意思了

singleton就是单独的人,独生子女,进而也有单例的意思了

先写一个引用

private static Singleton instance;

私有 静态 类型为Singleton,名称是instance(意思是实例)

这个,就是我们创建出的唯一实例的引用

因为是饿汉模式

就是已经饿的迫不及待了

程序一开始执行到类的时候就等不及的创建实例了

因此写为

private static Singleton instance = new Singleton();

但是要要创建实例,只能自己创建,因此构造方法也设置为私有的


private Singleton(){}

但是实例instance是私有的

那使用实例的时候怎么获取到呢

那就直接使用get

写个get方法,设为public


public static Singleton getInstance(){
    return instance;
}
 

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

饿汉模式的一个明显缺点是,它在启动时会一次性加载所有数据。例如,当我们使用编辑器打开一个非常大的文件(比如10GB),饿汉模式会将整个文件加载到内存中, 完成以后才能够进行统一的展示。这意味着用户必须等待文件完全加载后才能看到内容,这可能会导致长时间的延迟。

为了解决这一问题,懒汉模式应运而生。懒汉模式的关键在于“懒”这个字,即延迟加载。它的核心思想是只在需要的时候才加载数据。例如,编辑器在打开文件时,只会读取一小部分数据(比如10KB),并将这些内容首先展示给用户。随着用户的操作(如翻页),编辑器会继续加载后续的数据。这样不仅提高了用户体验,还节省了系统资源。

懒汉模式

老规矩,写一个实例

private static SingletonLazy instance

然后置为空

private static SingletonLazy instance = null;

但是和恶汉不一样的是,饿汉上来直接创建,懒汉先置为空

仍然是私有的构造方法

private SingletonLazy() {}

但是获取实例的方法稍微不一样了

饿汉可以直接获取,而我们懒汉要考虑的就很多了

想要获取实例的时候,能够直接return instance吗

当然那不行,因为现在还是null呢

所以咱们得先new一个

instance = new SingletonLazy();

但是每次访问都得重新创建一个吗

肯定不行

当instance不为null的时候就不new了

这个还是很简单的对吧

public static SingletonLazy getInstance() {
    if (instance == null) {
        instance = new SingletonLazy();
    }
    return instance;
}

这个时候就算做,如果是首次调用getinstance,instance引用为null,就会进入if条件,那么就会把这个instance实例创建出来并且返回

如果是后续再调用getinstance,由于instance已经不再是null了

这样设计,仍然可以保证,该类的实例是唯一一个

与此同时,创建实例的时机就不是程序驱动的时候了,而是第一次调用getinstance的时候

这个操作的执行时机就不得为知了,就看自己程序的实际需求,大概率比饿汉的方式要晚一些,甚至也有可能整个程序根本就没用到这个方法,就把创建的操作也省下来了

有的程序,可能是根据一定的条件,来决定是否要进行某个操作,进一步得来决定创建某个实例

就比如,肯德基有个操作是“疯狂星期四”

对于肯德基 点餐系统来说,就可以判定一下今天是星期几,

如果是星期四,才加载疯狂星期四相关的逻辑和数据

如果不是星期四,就不用加载了(节省了一定的开销)

现在写一个main方法测试一下代码

public static void main(String[] args) {
    SingletonLazy lazy1 = SingletonLazy.getInstance();
    SingletonLazy lazy2 = SingletonLazy.getInstance();
    System.out.println(lazy1 == lazy2);
}

创建了两个 SingletonLazy 实例 lazy1lazy2,如果它们是同一个实例,那么比较结果应为 true

结果显而易见,肯定是返回的true

谁是线程安全的?

现在加上多线程,看看单例模式和多线程能碰撞出什么样的火花

现在我要提出一个问题

饿汉模式和懒汉模式,谁是线程安全的?

首先,我们看看,饿汉模式,它的getinstance只有一个操作

只有一个读操作

本质上,多个线程对同一个变量进行读操作,是线程安全的!!!

现在看看懒汉模式呢:

既有读操作,也有写操作

那么多个线程对同一个变量进行写操作的时候,就不是线程安全的

比如下面这种执行顺序

(win11的画图功能咋这么糊)

此时就new出了两个实例

这就不符合单例模式的初衷了

这就不是单例模式了,这就有bug了

这就不是单例模式

单例模式的对象,可能非常大,一个对象可能要管理10个G的内存(我现在自己的电脑都才16G),要是new了两个,给我卡死了,我直接换一台新的(bushi)

于是上述讨论的过程,得出来的结论就是:

懒汉模式不是线程安全的,饿汉模式是线程安全的

怎么改进懒汉模式

怎么解决懒汉模式的线程安全问题呢?

怎么改进懒汉模式

(盲猜加锁)

可以在外面套一层锁

现在我随便把锁加在if里面

这样真的可以吗

这个锁是静态的,就算是两个线程也只有一个锁,加锁按理说成功了啊

现在要注意了啊

多线程水很深,很复杂,一点小小的变动,会导致得出来的结果完全不一样

现在咱们看看

还是不行,还是new了两个实例

这个锁白加了

(我希望我这个例子没有白写,万一面试官问到屏幕前的你的时候,为什么不在这里(if里面)加锁,你要能回答的上来)

那就把synchronize枷锁加到if外面呗

现在的执行顺序是

现在是线程安全的了嘛

线程1和线程2都只new了一个对象

问题已经迎刃而解了

但是上述代码仍然有问题,换而言之

咱们加锁的目的是什么,场景是什么

这个自己心里清楚吗

不就是因为第一次调用getinstance的时候会出现线程安全的问题吗

也就是说

只有第一次调用getinstance的时候

才需要加锁

其他时间都是不需要加锁的!!!

加锁意味着阻塞等待!

如果我们已经不是第一次调用getinstance了,已经没有线程安全了,

要是有很多进程同时调用一个没有线程安全的方法的话,就会有加锁操作,加锁操作意味着阻塞等待,时间就会浪费很多了

因此

我们再在外面判断一次instance是否为null

不为空就直接返回instance

而不是先加锁自己判断了以后再返回,这样其他线程只能阻塞等待再判断了

所以大家在加锁的时候,不要贪杯,不该加锁,就不能随便乱加,需要加锁的时候再加速

所以说除了StringBuilder还提供了StringBuffer,除了Vector还提供了ArrayList....

(StringBuffer线程安全,StringBuilders虽然线程不安全但是性能相对较高)

(Vector线程安全但是性能不如ArrayList)

最终版本:

public static SingletonLazy getInstance() {
    if (instance == null) {
        synchronized (lock) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
    }
}
    return instance;
}

别看这有两个一样的if判断 instance == null,在单线程/非阻塞的代码中确实是无意义的,但是在多线程环境下,这样的代码十分有意义

看起来是一样的条件,实际是,这两个条件的结果可能是截然相反的

synchronized可能让当前这个线程阻塞,阻塞的过程中,可能会有别的线程修改了instance,这两个if中间隔得时间,可能是沧海桑田...

比如:

就好比二十年前你坚定不移的说着你爱我,但是二十年后答案还会始终如一吗

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值