【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)

人生之路不会是一帆风顺的,我们会遇上顺境,也会遇上逆境,在所有成功路上折磨你的,背后都隐藏着激励你奋发向上的动机,人生没有如果,只有后果与结果,成熟,就是用微笑来面对一切小事。

导读:本篇文章讲解 【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

目录

什么是单例模式?

饿汉模式

懒汉模式

懒汉和饿汉,谁线程安全,为什么?

如何修改,让懒汉模式也线程安全?

 面试题1:上图中的两个if一模一样,为什么要判断两遍?

面试题2:下图中的volatile有什么用?


什么是单例模式?

        是一种给常见的设计模式,先来他谈谈何为设计模式,在代码领域里,很多程序员的水平参差不齐,于是就有大佬们根据一些常见的需求,整理出来的一些应对办法;那么单例就是指单个实例(对象),也就是说一个类只能有一个实例,例如,在中国,一个男人只能娶一个老婆是一个道理;

        单例模式,本质上就是借助变成语言的语法特性,强制限制某个类,不能创建多个实例。

        在Java中有些东西是天然的单例,例如static,他可以修饰成员/属性,也就是我们口中熟知的类成员/类属性,实际上这种叫法,也是有一定原因的,也就是这个类特有的成员和属性;更具体的来说,类对象是通过JVM针对某个.class文件只会加载一次,就只有一个类对象,包括类成员,都是靠static修饰,也就只有一份;


饿汉模式

        这个模式表示一个类在加载的时候就创建好实例了,“饿汉”一词便体现出创建这个实例是非常急迫,非常早的;

来看看具体代码:

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

    }
}

分析:

【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)

         用private修饰就是为了防止在类外对Singleton实例进行修改,static修饰保证了这个类无论加载多少次,都可以保证这个实例的创建只被加载一次;这里的instance便是Singleton的唯一实例,在类加载的时候,便已经创建好了

【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)

         这里便限制了如何拿到instance这个实例——只能通过getInstance这个方法才能拿到这个实例;

【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)

         将构造方法设置为private,之后在类外,就无法通过new再实例对象了(如下图),所以之后要使用这个唯一的实例,只能通过getInstance方法获得;

【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)


懒汉模式

        还有一种经典的单例模式叫“懒汉模式”,这里的“懒”是一个褒义词,创建实例来的更迟,但是效率更高;为什么效率更高呢?一般程序刚启动的时候,要初始化的东西有很多,系统资源紧张,所以构造实例这个过程实际上可以往后放放,当这个创建实例的过程晚了,跟其他耗时操作岔开了,初始化效率就高了,速度自然也就跟上了

代码如下:(跟多线程还没扯上关系,不是最优版本)

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

分析:

         刚开始是并没有直接创建实例,而是通过赋值null一笔带过,在真正需要实例的时候,通过调用getInstance方法来获取实例,若instance为空的时候,才去创建实例,不为空的时候说明实例已经创建好,直接返回即可

懒汉模式最佳写法:

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

以下将会一步一步分析这样写的原因


懒汉和饿汉,谁线程安全,为什么?

        咱们可以先来对比一下双方获取实例的方法:

饿汉模式

【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)

         这个操作只是单纯的“读操作”,不涉及修改;

懒汉模式

【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)

         这个操作即设计到读操作(装载,比较),也涉及到修改操作(new,赋值),在多线程情况下,就不安全了;

如下图:(创建对象简化为:NEW,赋值简化为:ASSGIN)

【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)

         分析(脏读):线程二在LOAD操作的时候,线程一还没有修改完,线程二读到还是旧数据,导致线程二CMP时,instance依然是null,所以依然可以NEW,最后实例就被创建了多份!而我们的需求是,让线程二读到的数据是线程一修改完后的数据…


如何修改,让懒汉模式也线程安全?

        把多个操作打包成一个操作(原子操作)——加锁;

如下代码:(还不是最高效的)

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

分析:

        通过这种加锁方式,就保证了线程安全(如下图)

【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)

        但是有引入了新的问题…

        每次都需要先加锁后,再判断是否创建实例,而加锁操作操作的开销实际上还挺大,加锁可能会涉及到 用户态->内核态 之间的切换,这样的切换成本是很高的,如何解决呢?再来分析一下刚刚的代码:懒汉式代码线程不安全,不是一直线程都不安全,而是在第一次调用的时候才会触发不安全,一旦实例创建好了,就不会有线程不安全问题了,也就是说,加锁操作,只需要在第一次调用的时候加锁即可!所以此时可以再加一个if语句;(如下图,还不是最高效,最后一张图才是)

【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)

 面试题1:上图中的两个if一模一样,为什么要判断两遍?

        第一个if是为了判断是否要加锁,第二个是用来判断是否创建实例,而这两个条件碰巧写法一样,但所表示的意义不同,为什么呢?这两代码中间隔着一个加锁操作,看起来是只隔了一行代码,实际上确隔了孙悟空一个跟头,加锁就可能产生竞争,竞争就会导致阻塞,一旦阻塞,什么时候唤醒就不知道了,所以再个阻塞时间里,一旦线程二创建instance实例,这个时候线程一的第二个if判断就不可省去,也就是说,第二个if和第一个if的结果可能是截然不同的,第一个if成立了,第二个if不一定成立;

面试题2:下图中的volatile有什么用?(懒汉模式完整代码)

【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)

 

 解释原因:

        假设这样一个场景,俩个线程同时调用getInstance方法,第一个线程拿到了锁,进入第二个if,开始new操作,而这里的new操作实际上是三步操作:

1.申请内存,得到内存的首地址;

2.调用构造方法,来初始化实例;

3.把内存的首地址赋值给instance引用;

        这样一个场景之下,可能就会触发指令重排序,例如执行顺序变成了1、3、2 ,再单线程下,3和2是可以相互调换顺序的,没有什么影响,但是在刚刚假设的场景之下调换了3和2的顺序,就有问题了;假设此时触发了指令重排序,按照1、3、2的顺序执行,那么如果线程2在线程1执行了1,3之后(得到了一个空的对象,只分配了内存,数据是无效的),执行2之前,调用了getInstance方法,这时线程2就会进入第一个if,判断instance为非空,就返回了instance,而后续操作一旦涉及到对instance进行访问,就会得到无效数据;这里便是指令重排序带来的问题,要解决这个问题,就需要用到volatile,这个关键字既能保证内存可见性,也能禁止指令重排序,上述代码,就完成了完整的单例模式的懒汉实现


【多线程】深入理解,单例模式:饿汉模式和懒汉模式(附常考面试题)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/124327.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!