美文网首页
求职字节跳动!一道简单的Java单例模式问题,问完我懵逼了...

求职字节跳动!一道简单的Java单例模式问题,问完我懵逼了...

作者: 阿博的java技术栈 | 来源:发表于2020-04-27 16:31 被阅读0次

    1.场景:

    面试官:有用过单例模式吗?

    我:有有有(自信满满)。

    面试官:说说单例模式几种写法?

    我:懒汉式和饿汉式,懒汉式巴拉巴拉,饿汉式巴拉巴拉。

    面试官:我们都知道synchronized加锁是比较耗费资源的,你这种写法每次访问都需要获得锁(基础的懒汉式写法),效率比较低,有什么优化的方式吗?

    我:沉思片刻,脑海灵光一现。可以采用双重检查加锁的方式,巴拉巴拉。(还好之前看到过,暗自庆幸)

    面试官:为什么双重检查加锁需要加volatile关键字?

    我:要不我们问问度娘?

    在回答这个问题之前我们要明确这几点,一个是对象的创建过程,一个是什么是指令重排,一个是CPU时间片的概念,一个是synchronized不会禁止指令重排,最后一个是volatile禁止指令重排。

    2.对象的创建过程

    对象的创建过程主要分成三步,如下图展示的汇编码所示,主要是0,4,7这三步。

    0 这步是为新创建的对象申请内存,但是此时对象中的成员变量的值是默认的值(半初始化),即下图a 的值此时是0;

    4 初始化对象,在这步才把10赋给成员变量a

    7 建立关联,把testDemo引用和new 出来的TestDemo对象建立关联

    
    public class TestDemo {
        private int a = 10;
        public static void main(String[] args) {
     
            TestDemo testDemo = new TestDemo();
    //        0 new #2 <company/syncronized/TestDemo>       申请内存,半初始化,此时a的值是0(当对象刚new出来的时候会给里面的成员变量设置默认初始值,int类型的初始值是0)
    //        3 dup                                         复制
    //        4 invokespecial #3 <company/syncronized/TestDemo.<init>>    初始化,在这步把10赋给a,此时a的值是10
    //        7 astore_1                                 testDemo和new TestDemo()建立关联
    //        8 return
     
            
        }
    }
    

    3.指令重排

    指令重排是JMM(java内存模型)中的一个概念,它是指计算机在执行程序时,编译器和处理器会对不存在数据依赖性的指令进行重新排序。

    什么是数据依赖性:就是A,B两个指令,B指令的执行依赖A指令的执行,举个简单的例子,看下面的代码,语句2需要语句1申明a变量之后才能使用,那么它们之间就存在数据依赖性。

    什么是指令重排:一般情况下rely()方法的执行顺序是1,2,3,4顺序执行,但是在编译器和处理器的优化下,执行顺序有可能变成1,3,2,4,也有可能是3,1,2,4,这个就是指令的一个重排。那么有没有可能出现1,4,2,3的情况呢,不会出现,现有3,4存在数据依赖,所以3必须在4之前被执行。

    public void rely(){
            int a = 10;   //语句1
            a = a +1 ;    //语句2
     
            int b = 8;    //语句3
            b = b +1;     //语句4
     
        }
    
    

    4.CPU执行时间片

    时间片定义:时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。

    产生的问题:根据上面的时间片定义,我们可以得出,CPU不会等待一个进程执行完毕之后在执行下一个进程,而是CPU会给每一个进程一段执行的时间,这段时间结束,就会轮到下一个进程去使用CPU,这样会出现一个上面问题呢?就是我的进程可能执行到一半,时间片时间到了,CPU给下一个进程了,那么我的进程在这个时间片内就只执行了一半,需要等待下次占有CPU的时候才能把全部的进程执行完毕。也就是说,一个进程可能是需要多个CPU执行时间片的时间来完成。

    5.指令重排对双重检查加锁模式的影响

    下面进入我们的正题。经过刚才的分析,我们知道创建对象是分成三步走,看下图的1,2,3。那么在这个创建过程中,有没有可能会发生2,3指令重排序?答案是肯定的,因为synchronized它是不会禁止指令重排,假设2,3指令发生了顺序交换,也就是test 引用先和 new SingleDemo对象建立连接,然后在初始化new SingleDemo,那么此时问题就产生了。

    之前我们提到cpu时间片的概念,那么我这个进程如果正好执行到test引用建立连接,cpu时间片时间到了,然后轮到下一个进程来执行。好的,那么小伙伴最容易困惑的点来了,synchronized不是保证原子性吗?我的线程还没有执行完毕,我还握着这把锁,就算下一个线程进来也是阻塞的状态,不会对我产生影响。只有当我下一个获得cpu,然后执行完毕,释放锁,那么下个线程才能进行操作,那么既然当前线程一定会执行完毕,那么顺序交换也没有影响了吧。是的,如果我当前线程执行完毕,确实没有影响,但是我下一个进程进来不是阻塞呢?

    问题的关键来了,我第一个线程A进来之后,new singledemo()过程发生了指令重排,初始化和建立引用联系的顺序换了,我先建立了引用联系,也就是说此时test 引用指向了一个没有初始化,只有半初始化状态的new Singledemo对象,也就是说此时test是不为空的,然后这时候我cpu时间片的时间到了,然后当前线程A让出cpu,但是当前线程A仍然持有锁。我下一个线程B进来之后,在外层if(test==null)条件下进行判断,然后发现我test对象里面是有东西的,然后就直接return了,也就是说线程B已经获得了一个初始化的new Singledemo对象,你线程A锁着就锁着吧,我线程B拿到对象了。那么此时会产生一个什么问题,就是线程B中的成员变量信息是错误的,我本来a=10,你拿到半初始化状态对象的a=0,那么我在使用这个成员变量的时候就是不正确的。

    那么怎么解决指令重排序的问题呢?加volatitle关键字,这样可以保证在new singledomo过程中不发生指令重排。

    
    public class SingleDemo {
        //private volatile static SingleDemo test =null;
     
        private static SingleDemo test =null;
        private int a = 10;
     
        public int getA() {
            return a;
        }
     
        private SingleDemo(){}
     
        //双重检查加锁模式
        public static SingleDemo get2(){
            if(test==null) {                                       //外层检查是问题关键
                synchronized (SingleDemo.class) {
                    if (test == null) {
                        test = new SingleDemo();
                        //new singleDemo对象,半初始化              //1
                        //初始化                                   //2
                        //test引用 和 new singleDemo对象建立连接    //3
                    }
                }
            }
            return  test;
        }
    }
     
    

    最后,看完了记得收藏和转发,然后私信哦~

    其实针对于Java程序员,我这边还给大家准备了免费的Java面试资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的大厂面试真题)

    类似于相关的PDF都是免费分享的,希望能够帮助到有需要的朋友,同时也节省了大家再去网上找资料的时间。

    相关PDF获取请转发本文后私信资料 即可免费领取!

    之后小编也将会继续给大家分享更多我在大厂面试碰到的真实案例,关注我上车别跟丢!

    相关文章

      网友评论

          本文标题:求职字节跳动!一道简单的Java单例模式问题,问完我懵逼了...

          本文链接:https://www.haomeiwen.com/subject/aifywhtx.html