单例模式:懒汉&饿汉&线程安全问题

在我们前几篇文章中都了解了一些关于线程的知识,那么在多线程的情况下如何创建单例模式,其中的线程安全问题如何解决?


目录

1.什么是单例模式? (饿汉模式)

2.单例模式(懒汉模式)

*懒汉模式与懒汉模式的对比

*如何解决懒汉模式下线程不安全问题?


1.什么是单例模式? (饿汉模式)

单例模式:某个类,在进程中只有唯一的实例,不能new多次。例如如下代码:

java 复制代码
class Singleton{
    private static Singleton singleton = new Singleton();

    public static Singleton getSingleton() {
        return singleton;
    }
    //将构造方法设置为private禁止外部重新创建
    private Singleton(){
        //空着就行
    }
}
public class ThreadDemo12 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getSingleton();
        Singleton s2 = Singleton.getSingleton();
//        外部不允许再new这个类的实例
//        Singleton s3 = new Singleton();
    }
}

我们通过操作,来禁止外部在产生新的实例

1.将成员变量Singleton设置成static.标志着次成员变量是类的静态成员.那自然只有一份.

2.将类的构造方法设置成private.那外部就不能再创建其他的实例本体了.

3.提供getter方法,只有调用getter方法才能获取唯一实例本体.

我们可以发现s1,s2这两个实例是同一个,不信的玉粉可以打印一句:sout(s1==s2);看是否是true.但是s3就不可以了.这种单例模式我们称为**"饿汉模式".意味着我们在新建成员变量的时候,就new实例,只是在调用getter方法的时候返回这个实例本体.**下面我再介绍一种单例模式:"懒汉模式".

2.单例模式(懒汉模式)

观察懒汉模式的例子:

java 复制代码
class SingletonLazy{
    private static SingletonLazy singleton = null;
    private SingletonLazy(){};//构造方法

    public static SingletonLazy getSingleton() {
        if(singleton == null){
            singleton = new SingletonLazy();
        }
        return singleton;
    }
}
public class ThreadDemo13 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getSingleton();
        SingletonLazy s2 = SingletonLazy.getSingleton();
        System.out.println(s1==s2);
        //不能执行新的new操作
//        SingletonLazy s3 = new SingletonLazy();
        
    }

懒汉模式就意味着:我们在创建成员变量的时候,先不进行new操作,先让其=null;然后再调用getSingleton()方法的时候,去判断它是否为null,如果为null,代表着当前成员未被创建,就new;否则代表当前对象已经存在就返回这个对象即可.

*懒汉模式与懒汉模式的对比

观察上述两个案例代码思考一个问题:
++在多线程模式在哪个模式有线程安全问题?懒or饿or都有or都没有? why?++

正确答案是: 饿汉模式没有线程安全问题,懒汉模式有线程安全问题!!!

那么为什么会这样呢? 我们要注意,此处有个大前提:多线程下!!! 在多线程环境下,我们饿汉模式突出的是"急迫""急需",而懒汉模式顾名思义就是"非必要不创建""从容".试想在线程快速调度的情况中,饿汉模式是有优势的:无论怎么调度,我上来就new,

复制代码
private static Singleton singleton = new Singleton();

++这一行代码是原子性的,无法拆分,这样就能保证我的对象只能被new一次.在对应的getSingleton()方法中也是"只读"操作++,我们说过"只读"情况下是没有线程安全问题的而懒汉模式则是先不new,需要了再new,但是你new的时候有经过一系列的判断,新建new然后返回,万一在途中被切走了,那你new的对象就不止一个了.

*如何解决懒汉模式下线程不安全问题?

那么我们需要寻找到一中解决办法:保证懒汉模式在多线程环境下线程安全问题:有以下三步:

  1. 为了保证原子性,需要加锁.
  2. 为了防止线程多次调度下创建多个对象,需要双重 if 判定.
  3. 为了禁止指令重排序,需要加上volatile关键字.

下面我们分别来解析其中的道理:

1.加锁

我们可以直观的对比出来:懒汉模式与饿汉模式最大的区别就是:++饿汉模式的new操作是原子性的.++ 那么我们已经熟悉过可以打包代码的方法-----加锁,那么第一步就是给getSingleton()方法加锁,这里你++既可以给方法前缀加上关键字synchronized,也可以在if 判断的时候加上synchronized++,但是万万不可以这样加锁:

这样相当于没加,因为你要确保你的原子性是if 判定所包含的所有内容,所以稳妥的办法是在if 外面加锁:like that:

千万注意别把锁加错位置了!!! 还是不明白的玉粉可能你需要仔细研究一下"懒汉模式与饿汉模式的区别"......

2.双重if 判定.

那么我们为什么需要双重if 判定呢?双重 if 怎么写呢?来看正确案例:

有些玉粉可能就疑惑了:俩if 判定一模一样啊????为什么??? 这里我要强调一下:不是if 长的一样就代表一个意思,也不是代码赘余了,这两个if 有不同的初心!!!在多线程的环境下,第一个if判断的是"是否要加锁", 因为加锁操作实际上是非常低效的操作,加锁就可能有阻塞,如果没有第一个if判定,那么我们++只要调用getSingleton()方法就会触发"锁竞争"++ ,是非常不友好的.第二个if判断的是,线程无论是否经历了调度,加锁后的singleton是否还是null. 因为++在两个if判定中间有加锁操作,加锁意味着有可能出现"锁竞争",有可能会发生"阻塞",等到真加上锁了,其中线程可能已经被切换了N次++,那么这时候就有种"士别三日""如隔春秋"的感觉了,这时候的singleton是不是还未被其他线程创建就不得而知了,那就必须再次判定,如果"此singleton"还是"彼singleton"那就继续new吧,如果不是就直接返回singleton对象了.....

3.volatile关键字

这里是小玉一直不太懂的地方,现在终于懂了也希望和大家分享一下心得:在讲加锁操作关键字synchronized的时候,我们说synchronized能禁止指令重排序这个说法存疑!!! 不然我们发明什么volatile干什么?volatile才是明确的1.用来保证内存可见性2.用来禁止指令重排序 ,但是在多线程创建对象的时候不存在什么"内存可见性"这一说,所以它再次的作用只是用来禁止指令重排序的.
试想一下:在你创建对象new操作的时候,大致分为三步:1.申请内存. 2.调用构造方法初始化. 3.返回对象地址. ++指令重排序可能会让new操作从正常的123变成132.++如果执行顺序真的是132,那么1完成之后该3了,此时线程被调度走了,其他线程可能会以为该对象是完整的对象,那么在访问它的属性的时候,就会发现它其实是一个没有初始化的"空壳子",里面没有方法没有属性...什么都干不了......所以禁止指令重排序是必要操作,那么更改完的代码如下:

java 复制代码
class SingletonLazy{
    volatile private static SingletonLazy singleton = null;
    private SingletonLazy(){};//构造方法

    public static SingletonLazy getSingleton() {
        if(singleton == null) {
            synchronized (SingletonLazy.class) {
                if (singleton == null) {
                    singleton = new SingletonLazy();

                }
            }
        }
        return singleton;
    }
}
public class ThreadDemo13 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getSingleton();
        SingletonLazy s2 = SingletonLazy.getSingleton();
        System.out.println(s1==s2);
        //不能执行新的new操作
//        SingletonLazy s3 = new SingletonLazy();

    }

}

如此就没有线程安全问题了...............


好了小玉先讲这么多,其实小玉在这一篇想讲一下++"阻塞队列""生产者消费者模型"++ 的,因为看了b站 的一个视频印象很深刻,感觉很有东西可以讲,所以就文思泉涌想开始写,但是没有单例模式的铺垫很难讲好这些东西,所以就换成了将单例模式及懒汉&饿汉了.whatever,小玉下一章就可以将这些内容了,过年了小玉有些偷懒,最近心情也不是很好,有一个繁琐的事对心境造成了影响,写博客可以说是我的排解途径之一吧......期待小玉吧! 小玉会继续努力的!!!!!!!

在此祝大家新年快乐,龙年小玉在实现自己的梦想,希望大家&玉粉也能梦想成真!!!

相关推荐
秃头佛爷25 分钟前
Python学习大纲总结及注意事项
开发语言·python·学习
阿伟*rui26 分钟前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
待磨的钝刨26 分钟前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei2 小时前
java的类加载机制的学习
java·学习
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉4 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端