什么是单例模式?

发布于:2024-07-04 ⋅ 阅读:(13) ⋅ 点赞:(0)

  前言👀~

上一章我们介绍了多线程下引发的安全问题,今天接着讲解多线程的内容,同样很重要,请细品

单例模式

饿汉模式

懒汉模式

饿汉模式和懒汉模式的区别

再遇线程不安全问题

指令重排序问题


如果各位对文章的内容感兴趣的话,请点点小赞,关注一手不迷路,讲解的内容我会搭配我的理解用我自己的话去解释如果有什么问题的话,欢迎各位评论纠正 🤞🤞🤞

12b46cd836b7495695ce3560ea45749c.jpeg

个人主页:N_0050-CSDN博客

相关专栏:java SE_N_0050的博客-CSDN博客  java数据结构_N_0050的博客-CSDN博客  java EE_N_0050的博客-CSDN博客


单例模式

开发过程中,会遇到很多经典场景,就是经常出现这种场景,针对这些频繁出现的场景,提出了这种设计模式,遇到什么场景我们就用什么设计模式

1.单例模式(重要):非常经典的设计模式(我们需要掌握的技能)

单例就是指单个实例也就是单个对象,有些场景我们希望有的类只能有一个对象,不能有多个,这样的场景下,只能使用单例模式。例如按常理说每个男人或者女人只能有一个老公或者老婆。代码中很多用于管理数据的对象,就是单例的,例如Mysql JDBC中的DataSource(描述了mysql服务器的位置),JDBC 中的 DataSource 实例就只需要一个实例

像单例模式这样的思想,在很多地方有体现,例如final修饰的常量修改了会报错,以及接口实现了接口就必须重写里面的所有方法不然会报错等。但是呢,在语法上,没有对单例做出支持,只能通过编程技巧来达成类似的效果,此时我们需要编译器帮我们做出监督,如果创建出多个对象,编译器直接报错

下面我们设计一个单例模式:

1.在类的内部,提供一个获取当前类的实例

2.使用private修饰构造方法,避免其他类创建出这个类的实例

class Singleton {
    public static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {
        
    }
}

这样设计后,别人只能通过这个类的实例使用这个类,不能创建这个类的实例


饿汉模式

单例模式具体的实现方式, 分成 "饿汉" 和 "懒汉" 两种

饿汉模式:创建时机比较早,在类加载的时候就创建了因为这里使用了static修饰,我们知道static修饰的成员在类加载的时候就创建了,相当于你三天没吃饭了,看到吃的立马冲上去哐哐吃

和上面我们设计的单例模式一样,那个代码就是饿汉模式的代码实现

class Singleton {
    public static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {

    }
}

懒汉模式

懒汉模式:创建时机比饿汉模式晚,在第一次使用的时候也就是调用获取实例的方法,才去创建实例,没调用就没创建

下面是实现懒汉模式的代码

class SingletonLazy {
    public static SingletonLazy instance = new SingletonLazy();

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

    private SingletonLazy() {

    }
}

饿汉模式和懒汉模式的区别

下面我举了两个例子

例子1:拿程序举个例子,就比如我们打开一个笔记软件(20个G),有两种情况,一种是一下子把所有数据都加载到内存中,加载完后才显示内容,这个加载的过程很慢(饿汉)。还有一种就是加载一部分到内存,你打开就能看到一部分内容,然后你想看其他地方的内容你一点击,它就会加载到内存然后显示内容(懒汉)

例子2:比如懒汉模式就是我们回到家想着要去洗澡,但是呢有点累想休息会再去洗,于是打开了手机在那刷短视频,一刷就停不下来了,直到你发现时间很晚了才去洗。饿汉模式就是我们回到家直接就去洗澡,即使你很累,但是对于有些人它会立马去洗澡再去干其他事。对于我来说,我就属于懒汉模式,我觉得大多数人都是懒汉模式


再遇线程不安全问题

之前说过多个线程,同时修改同一变量(共享变量),此时就可能出现线程不安全的问题,多个线程,同时读取一个变量,这个是没事的

多线程情况下,饿汉模式线程安全因为它的实例在类加载的时候就创建了,其他类使用的时候直接调用方法去使用即可(直接读取即可),但是呢,多线程情况下,懒汉模式可能会造成线程不安全(又读又修改),下图进行演示

解释:懒汉模式的实例在获取实例的时候才会创建,可能会导致t1这个线程获取实例的时候然后进行判断,此时t2线程已经创建了实例,然后轮到t1线程接着执行后面的代码,又创建了一个实例,这就违背了单例模式的要求

如何去解决呢?想到的办法肯定是加锁,这样就能保证在一个线程在创建实例的时候,另外一个线程进入阻塞等待(锁的互斥)下面是加锁的代码

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

但是呢又会有个问题就是加锁,导致我们的性能降低,并且懒汉模式线程安全问题只是出现在最开始还没创建出实例的时候,,创建出来了就没有这样的问题了。所以每次获取实例的时候都要加锁,虽然解决了线程不安全的问题,但是性能不高,加锁和解锁是一个开销很大的操作,并且加锁可能会涉及到锁冲突/锁竞争问题,冲突就会造成阻塞等待

解决方法:加个if语句进行判断,这样就算一个线程在cpu上执行一半被换成其他线程也没事,其他线程创建完实例后,再次回到cpu上执行的时候,进入加锁然后判断,此时已经有实例了,直接返回实例即可,因为我们知道对同一把锁加锁两次会造成锁冲突,导致另外一个线程进入阻塞,这时候两个if就起了重要作用,下面是代码演示

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

指令重排序问题

指令重排序:也是编译器优化的问题,编译器为了执行效率,可能会调整当前代码的执行顺序,但是调整的前提是保持逻辑不变。注意一下这里说的编译器是java中的编译器

什么意思?举个例子就比如你今天在学校完成这几件事吃饭、拿快递、跑步。你可以选择先吃饭再去拿快递再去吃饭,也可以选择先跑步再拿快递再去吃饭。但是呢你寝室离操场和快递站近,离食堂远,这时候选择先跑步再拿快递再去吃饭这样的效率更高。所以我们可以看出一般情况下,指令重排序在保持逻辑不变的情况下,可以使我们的执行效率提高。单线程没什么问题,但是多线程下可能会出现问题。我们单例模式中懒汉模式就会出现这种问题,看下面这段代码

class SingletonLazy {
    public static SingletonLazy instance = new SingletonLazy();
    
    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();//new对象的时候会进行优化
                }
            }
        }
        return instance;
    }

    private SingletonLazy() {

    }
}

指令重排序问题:在这段代码中编译器可能会对new操作创建对象操作进行优化,首先创建对象可以分为三步,第一步申请内存空间,第二步在内存上也可以说堆,在内存上创建对象然后进行初始化就是使用构造方法,第三步把内存的地址赋值给instance引用。对于创建对象这三个步骤的顺序是可以调整的,123或者132都行,但是第一步一定是先执行的,不然都没有内存空间你把对象搁哪呢?然后在t1线程中编译器开始操作先进行1和3步骤在准备要执行第二步的时候,然后这个时候cpu开始搞事情了执行t2线程,t2线程一进来不是加锁的操作而是if判断,这里注意一下锁的阻塞等待,一定是两个线程都加锁的的时候才会触发。所以可以执行if判断,然后判断直接返回instance引用。这时候这个引用指向了没有初始化的非法对象如果要调用方法或者属性可能会出bug
 

针对上述问题,我们使用volatile关键字来禁止编译器进行优化,这样指令重排序问题就解决了

class SingletonLazy {
    public static volatile SingletonLazy instance = new SingletonLazy();//使用volatile关键字

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

    private SingletonLazy() {

    }
}

以上便是本章内容,单例模式在设计模式中还是中还是很常见的,虽然内容不多,但是有很多注意点要注意,我们下一章再见💕