【多线程】实现一个线程安全的单例模式

1.什么是单例模式

单例模式是设计模式中的一种,其实设计模式就好好比是一个棋谱,我们在日常下棋的时候会有一些经典的套路。那么在设计模式中也有这样的经典套路。这些经典的套路都是有大佬前辈们实现的。我们在写代码的时候,有很多经典的场景,在经典场景中有一些经典的应对套路。大佬们把这些常见的应对手段给整理起来,就起来个名字–设计模式。有了设计模式,无论是新手程序员还是资深的老程序员,都会有一个代码编程规范。以便让初出茅庐的新手,代码不至于写的很糟糕。

2. 单例模式的组成

单例模式分为:饿汉模式懒汉模式。单例模式之所以被称为单例模式,是因为我们在创建单例模式类的时候,就把该类的构造方法使用private进行修饰,以便在该类外,不能直接创建出一个实例。

3.饿汉模式实例

饿汉模式:指的是在单例模式中,在对单例进行初始化的时候,直接赋予单例实例,直接new出一个对象。

饿汉模式也可以这样理解:我们平时在自己家里的时候,都洗过碗吧。就比如说,中午这顿饭使用了4个碗,在吃完饭后,我们立即就把4个碗给刷了。这里之所以被称为饿汉模式是因为,在饿汉模式中,创建实例的时候比较着急。在初始化的时候,直接创建实例。

饿汉模式代码案例:

//创建出一个单例模式
    //单例模式分为饿汉模式和懒汉模式
class Singleton{
    //在饿汉模式中,在进行初始化的时候,直接创建出实例
    public static Singleton instance = new Singleton();
    //使用private 修饰该类的构造方法,在类外无法创建出一个该类的对象
    private Singleton(){}
    //在类外调用该类中的实例
    public static Singleton getInstance(){
        return instance;
    }
}    
public class TestDemo2 {
    public static void main(String[] args) {
        //Singleton singleton = new Singleton();
        Singleton single = Singleton.getInstance();
    }
}

我们尝试在main类中,自己创建出一个SingleTon的实例。

在这里插入图片描述

3.1在饿汉模式中为什么在创建实例的时候使用static修饰?

因为 static 修饰的成员更准确的说是类成员,类属性、类方法,不加 static 修饰的成员准确的来说,就是实例方法,实例成员,实例属性。

在一个java程序中,一个类方法只会存在一份(JVM保证的) 这也就是为什么要使用static对实例进行修饰的原因。进一步的就保证了在类的static 成员也只会存在一份。

在这里我们在深究一个static 关键字

其实在我们使用的编程语言java中,static表示的意思和这个单词的字面意思完全不同,static 的意思大家知道 是静态的。这其实是一个历史遗留问题。

在C语言中的static有3个作用:

  • 修饰局部变量,把局部变量的生命周期变长。修饰一个全局变量,把这个全局变量的作用域限制到整个.c文件。

  • 修饰一个函数,把这个函数的作用域限制到整个.c文件。

    我们在这里也可以看出在c语言中static 关键字的英语本意和在c语言中的使用效果,也是对不上号的。

    其实在上古时期,那时候的static是表示把变量放到静态内存区中,于是引入了static关键字,但是随着计算机的发展,这个东西就逐渐的没落了。但是static 关键字有被赋予了新的功能。

    在C++中 static关键字除了上述C语言的static 功能之外还有新的用法,修饰一个类的成员变量和成员函数,此处static 修饰的成员就表示为类成员。

    Java语言就是把C++中static 的功能继承过来了而已。

既然static 关键字的本意和它的对应效果对不上号,那么为什么不使用其他的词呢?

在一个编程语言中,要想新增一个关键字,是一件非常有风险的事情。因为不能还在程序中的单词重合。

SingleTon.class 类对象,就是.class文件被JVM加载到内存中,表现出来的模样。类对象就有着.class文件的所有信息。就像类名,属性等都可以有SingleTon.class中找到。这样也就实现了反射

3.2 判断该实例是否是线程安全的

饿汉模式是线程安全的

那么为什么饿汉模式样式的单例模式是线程安全的呢?我们在程序的哪里判断该单例模式是线程安全的?

线程安不安全,具体是在多线程环境之下,并发调用的getInstance()方法是否会产生bug?

在博主的上一篇文章中,介绍了产生线程不安全的案例。

造成线程不安全的案例有5种。

  1. 线程抢占式执行,线程间的调度充满了随机性。
  2. 多线程对一个变量进行修改。
  3. 针对变量操作不是原子的
  4. 内存可见性问题
  5. 指令重排序问题。

我们现在回顾在饿汉模式中的getInstance()方法,在该方法中只有一个return操作,就是对一个变量进行了读取,符合针对变量操作的原子性。所以是线程安全的

4.懒汉模式实例

懒汉模式创建懒汉模式的单例模式的时候,我们不着急创建出实例。

还是那洗碗举例,我们中午吃饭的时候,使用了4个碗,吃完饭后,我们不着急洗碗,到了晚上吃饭的时候,需要使用的几个碗,那么我们现在就洗几个碗。假如说我们晚上要使用2个碗,那么就洗两个,剩下的两个碗不管。

在我们平时生活中,饿汉模式比懒汉模式好,因为你试一试中午吃完饭不洗碗,如果不洗碗肯定会被挨骂。但是在我们的计算机中可未必,懒汉模式要比饿汉模式要好一些。

那么为什么在计算机中懒汉模式要比饿汉模式要好一些呢?

就比如说,现在有一个1G的图片文件,如果按照饿汉模式,那么计算机就会在内存中一下把这1G大的图片文件全部加载出来,这不就耗费CUP资源,并且如果计算机用户在浏览图片文件的时候,就看了一点没有全部把图片文件浏览完。那么这样的饿汉模式不就是费力不讨好好吗?

反而看看我们的懒汉模式,之所以懒,是因为你赶它一些它走一下。在懒汉模式中,我们一次不会再内存中把所有的图片加载完,而是把计算机用户的一个计算机屏幕中的图片加载出来。用户滑动一下滚动条,加载一下。这样就进行了优化。

其实在计算机中懒汉模式是褒义词,但是在现实世界中就算了吧😂

class Singleton2{
    //懒汉模式,在该模式中不着急创建出实例,在类外需要的时候,我们再进行创建
    public static Singleton2 instance = null;
    private Singleton2(){}
    public static Singleton2 getInstance(){
        if(instance == null){
            instance = new Singleton2();
        }
        return instance;
    }
}    
public class TestDemo3 {
    public static void main(String[] args) {
        Singleton2 singleton2 = Singleton2.getInstance();
    }
}

4.1 判断该实例是否是线程安全的,如果不是线程安全的,那么怎样修改可以成为线程安全的实例

首先上述的所谓的懒汉模式的单例模式不是线程安全的

那么为什么它不是线程安全的呢?

因为在多线程中,我们调用懒汉模式中的getInstance()方法的时候,针对变量的操作不是原子的,那么有从哪可以看出不是原子的呢?

如图:

在这里插入图片描述

那么针对变量操作不是原子性的,它的解决办法就是进行加锁,使用synchronized关键字进行加锁!!!

修改之后的代码:

//实现一个线程安全的单例模
class Singleton2{
    //懒汉模式,在该模式中不着急创建出实例,在类外需要的时候,我们再进行创建
    public static Singleton2 instance = null;
    private Singleton2(){}
    public static Singleton2 getInstance(){
        synchronized(Singleton2.class) {
            if (instance == null) {
                instance = new Singleton2();
            }
        }
        return instance;
    }
}
public class TestDemo3 {
    public static void main(String[] args) {
        Singleton2 singleton2 = Singleton2.getInstance();
    }
}

我们知道如果遇到可针对变量操作不是原子的,要使用synchronized关键字进行加锁,但是也不是说,代码中有了synchroniezd关键字就一定不会线程安全,我们要把synchronized关键字加对地方。synchronized加的位置正确,不能随便写。

//类对象在一个类中只有唯一一份,就能保证调用的getInstance的时候都是针对都一个对象进行加锁
synchronized(SingleTon2.class){

}

但是我们加锁之后,又带来的新的问题!!!

对于刚才的这个懒汉模式的代码而言,线程不安全发生在instance没有被初始化之前,未被初始化的时候,多线程调用getInstance()方法时,会存在线程安全问题,因为涉及到读和修改。但是在instance初始化之后,instance一定不是null,if条件一定不成立,getInstance()就只剩下两个读操作,也就是说instance初始化之后,线程就是安全的了。

并且按照上述的加锁操作,无论是代码中的instance初始化之前,还是初始化之后。每次调用getInstance()方法的时候,都会对其进行加锁。也就意味着即使初始化之后(已经线程安全了),但是仍然存在大量的锁竞争。

既然这里的instance已经被初始化过了,即使这里的条件在不能被满足了,但是仍然会调用getInstance()方法,都需要进行加锁,也就可能会产生锁竞争,但是我们知道这里的锁竞争其实是没有必要的。

我们知道加锁确实能让线程安全,但是同时也付出了代价,一旦在一个线程中加了锁之后,那么就和运行高效无关了。(程序的速度就变慢了)因为加锁之后,线程之间是串行执行的。代码的运行效率就变慢了。

博主以前说过开发效率要比运行效率更重要,一切都要从程序员的利益出发,但是运行效率也不是说不重要!如果说运行效率不重要的话,那么我们在前面学习那么多的数据结构干啥,不都是使用一个较好的数据结构,来组织数据,让代码变得有效嘛

改进方案:

在instance初始化之前,才进行加锁,在初始化之后,不进行加锁。在加锁这里在加一个条件判断即可

代码如下:

//实现一个线程安全的单例模
class Singleton2{
    //懒汉模式,在该模式中不着急创建出实例,在类外需要的时候,我们再进行创建
    public static Singleton2 instance = null;
    private Singleton2(){}
    public static Singleton2 getInstance(){
        if(instance == null) {  //如果instance被初始化过了,那么就不必再进行加锁,直接返回这个实例即可
            synchronized (Singleton2.class) {
                if (instance == null) {
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}
public class TestDemo3 {
    public static void main(String[] args) {
        Singleton2 singleton2 = Singleton2.getInstance();
    }
}

我们在上述的代码中,可以看到在getInstance()方法中,使用了两个条件判断语句,都是判断instance==null ,但是这两个条件判断语句的实际含义是千差万别。这两个添加判断长得一样,纯属是一个美丽的错误。

上面的条件判断是是否需要加锁。也就是说现在的instance是否已经被初始化过了

下面的条件判断是是否需要创建实例。

我们如果去掉了里层的条件判断语句那么就会变成:

//实现一个线程安全的单例模
class Singleton2{
    //懒汉模式,在该模式中不着急创建出实例,在类外需要的时候,我们再进行创建
    public static Singleton2 instance = null;
    private Singleton2(){}
    public static Singleton2 getInstance(){
        if(instance == null) {
            synchronized (Singleton2.class) {  //其实在这里加锁,就是加了个寂寞,在这里只针对设置实例加锁,在加锁语句的外面,还有istance == null 涉及到 读和判断,所以说加锁和没加一样,还是不符合原子性。
                    instance = new Singleton2();
            }
        }
        return instance;
    }
}
public class TestDemo3 {
    public static void main(String[] args) {
        Singleton2 singleton2 = Singleton2.getInstance();
    }
}

如果直接对getInstance()方法进行加锁,那么就是一个无脑加锁

此处博主告诉各位老铁,在上述的代码中,还存在一个问题。但是已经线程安全了呀,哪里还有错呢?我们在单例模式中使用volatile,主要是使用volatile可以进制指令重排序,从而保证程序的正常运行。

 public class Singleton {
    private Singleton() {}
    // 使用 volatile 禁止指令重排序
    private static volatile Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) { // 1
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 2
                }
            }
        }
        return instance;
    }
}

注意观察上述代码,我标记了第 ① 处和第 ② 处的两行代码。给私有变量加 volatile 主要是为了防止第 ② 处执行时,也就是“instance = new Singleton()”执行时的指令重排序的,这行代码看似只是一个创建对象的过程,然而它的实际执行却分为以下 3 步:

创建内存空间。
在内存空间中初始化对象 Singleton。
将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。

试想一下,如果不加 volatile,那么线程 1 在执行到上述代码的第 ② 处时就可能会执行指令重排序,将原本是 1、2、3 的执行顺序,重排为 1、3、2。但是特殊情况下,线程 1 在执行完第 3 步之后,如果来了线程 2 执行到上述代码的第 ① 处,判断 instance 对象已经不为 null,但此时线程 1 还未将对象实例化完,那么线程 2 将会得到一个被实例化“一半”的对象,从而导致程序执行出错,这就是为什么要给私有变量添加 volatile 的原因了。

总结一下:

实现一个线程安全的单例模式—针对懒汉模式

  1. 在正确的位置加锁
  2. 双重if判定
  3. volatile关键字
  • 11
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小周学编程~~~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值