关于java:Offer快到碗里来Volatile问题终结者

35次阅读

共计 8385 个字符,预计需要花费 21 分钟才能阅读完成。

微信公众号:大黄奔跑
关注我,可理解更多乏味的面试相干问题。

写在之前

Hello,大家好,我是只会写 HelloWorld 的程序员大黄。

Java 中并发编程是各个大厂面试重点,很多知识点艰涩难懂,经常须要结合实际教训能力答复好,面试没有答复好,则容易被面试官间接挂掉。

因而,大黄利用周末工夫,醉生梦死,整顿之前和面试官 battle 的面试题目。

因为并发变成问题切实是太多了,一篇文章不足以囊括所有的并发知识点,打算分为多篇来剖析,面试中的并发问题该如何答复。本篇次要围绕 volatile 关键字开展。

对于并发编程一些源码和深层次的剖析曾经举不胜举,大黄不打算从各方面开展,只心愿可能借用这篇文章沟通面试中该如何答复,毕竟面试工夫短,答复重点、要点才是要害。

面试问题概览

上面我列举一些大厂面试中,对于并发编程常见的一些面试题目,有的是本人亲身经历,有的是寻找网友的面经分享。

能够先看看这些面试题目,当初心中想想,如果你面对这些题目,该如何答复呢?

  1. volatile 关键字解释一下【字节跳动】
  2. volatile 有啥作用,如何应用的呢【京东】
  3. synchronized 和 volatile 关键字的区别【京东】
  4. volatile 原理具体解释【阿里云】
  5. volatile 关键字介绍,内存模型说一下【滴滴】
  6. Volatile 底层原理,应用场景【抖音】

能够看到 volatile 关键字在各个大厂面试中曾经成为了必考的面试题目。答复好了必然称为加分项,答复不好嘿嘿,你懂的。

面试回顾

一个身着灰色格子衬衫,拿着闪着硕大的????logo 小哥迎面走来,我心想,logo 还自带发光的,这尼玛必定是 p7 大佬了,然而刚开始咱们还是得淡定不是。

面试官:大黄同学是吧,我看你简历下面写可能熟练掌握并发编程外围常识,那咱们先来看看并发编程的一些外围常识吧。有听过 volatile 吗?说说你对于这个的了解。

记住:此时还是要从为什么、是什么、有什么作用答复,只有这样能力给面试官留下深刻印象。

大黄:面试官您好,volatile 是 java 虚拟机提供的轻量级同步机制,次要特点有三个:

  1. 保障线程之间的可见性
  2. 禁止指令重排
  3. 然而不保障原子性

面试中,必定不是说完这三点就完了,个别须要开展来说。

大黄:所谓可见性,是多线程中独有的。A 线程批改值之后,B 线程可能晓得参数曾经批改了,这就是 线程间的可见性。 A 批改共享变量 i 之后,B 马上能够感知到该变量的批改。

面试官可能会诘问,为什么会呈现变量可见性问题了。这个就波及到 Java 的内存模型了(俗称 JMM),因而你须要简略说说 Java 的内存模型。
面试官:那为什么会呈现变量可见性问题呢?

大黄:JVM 运行程序的实体都是线程,每次创立线程的时候,JVM 都会给线程创 建属于本人的工作内存 ,留神工作内存是该线程独有的,也就说别的线程无法访问工作内存中的信息。而 Java 内存模型中规定所有的变量都存储在 主内存中,主内存是多个线程共享的区域,线程对变量的操作(读写)必须在工作内存中进行。

面试中记得不要干说实践,联合一下例子,让面试官感到你真的把握了。下面的问题你抓住主内存、线程内存别离论述即可。

大黄:比方,存在两个线程 A、B,同时从主线程中获取一个对象(i = 25),某一刻,A、B 的工作线程中 i 都是 25,A 效率比拟高,片刻,改完之后,马上将 i 更新到了主内存,然而此时 B 是齐全没有方法 i 产生了变动,依然用 i 做一些操作。问题就产生了,B 线程没有方法马上感知到变量的变动!!

大黄可见性 Demo 演示小插曲

import lombok.Data;
/**
 * @author dahuang
 * @time 2020/3/15 17:14
 * @Description JMM 原子性模仿
 */
public class Juc002VolatileAtomic {public static void main(String[] args) {AtomicResource resource = new AtomicResource();
        // 利用 for 循环创立 20 个线程,每个线程自增 100 次
        for(int i = 0; i < 20; i++){new Thread(()->{for (int j = 0; j < 100; j++) {resource.addNum();
                }
            },String.valueOf(i)).start();}
        // 用该办法判断上述 20 线程是否计算结束,// 如果小于 2,则阐明计算线程没有计算完,则主线程临时让出执行工夫
        while (Thread.activeCount() > 2){Thread.yield();
        }
        // 查看 number 是否能够保障原子性,如果能够保障则输入的值则为 2000
        System.out.println("Result ="+resource.getNumber());
    }
}
@Data
class AtomicResource{
    volatile int number = 0;
    public void addNum(){number++;}
}

上面是运行后果:
后果如下:

Result = 1906

Process finished with exit code 0

面试官:volatile 能够保障程序的原子性吗?
大黄:JMM 的目标是解决原子性,但 volatile 不保障原子性。为什么无奈保障原子性呢?
因为上述的 Java 的内存模型的存在,批改一个 i 的值并不是一步操作,过程能够分为三步:

  1. 从主内存中读取值,加载到工作内存
  2. 工作内存中对 i 进行自增
  3. 自增实现之后再写回主内存。

每个线程获取主内存中的值批改,而后再写回主内存,多个线程执行的时候,存在很多状况的写值的笼罩。

大黄可见性 Demo 演示小插曲

用上面的例子测试 volatile 是否保障原子性。

import lombok.Data;
/**
 * @author dahuang
 * @time 2020/3/15 17:14
 * @Description JMM 原子性模仿
 */
public class Juc002VolatileAtomic {public static void main(String[] args) {AtomicResource resource = new AtomicResource();
        // 利用 for 循环创立 20 个线程,每个线程自增 100 次
        for(int i = 0; i < 20; i++){new Thread(()->{for (int j = 0; j < 100; j++) {resource.addNum();
                }
            },String.valueOf(i)).start();}
        // 用该办法判断上述 20 线程是否计算结束,如果小于 2,// 则阐明计算线程没有计算完,则主线程临时让出执行工夫
        while (Thread.activeCount() > 2){Thread.yield();
        }
        // 查看 number 是否能够保障原子性,如果能够保障则输入的值则为 2000
        System.out.println("Result ="+resource.getNumber());
    }
}
@Data
class AtomicResource{
    volatile int number = 0;
    public void addNum(){number++;}
}

后果如下:

Result = 1906
能够看到程序循环了 2000 次,然而最初值却只累加到 1906,阐明程序中有很多笼罩的。

面试官可能心想,好家伙,懂得还挺多,我来试试你的深浅。

面试官:那如果程序中想要保障原子性怎么办呢?
大黄:Juc(Java并发包简称)上面提供了多种形式,比拟轻量级的有 Atomic 类的变量,更重量级有 Synchronized 关键字润饰,前者的效率自身是后者高,不必加锁就能够保障原子性。

大黄可见性 Demo 演示小插曲

import lombok.Data;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * @author dahuang
 * @time 2020/3/15 17:43
 * @Description 利用 Atomic 来保障原子性
 */
public class Juc003VolatileAtomic {public static void main(String[] args) {AtomicResource resource = new AtomicResource();
        // 利用 for 循环创立 20 个线程,每个线程自增 100 次
        for(int i = 0; i < 20; i++){new Thread(()->{for (int j = 0; j < 100; j++) {resource.addNum();
                }
            },String.valueOf(i)).start();}
        // 用该办法判断上述 20 线程是否计算结束,如果小于 2,// 则阐明计算线程没有计算完,则主线程临时让出执行工夫
        while (Thread.activeCount() > 2){Thread.yield();
        }
        // 查看 number 是否能够保障原子性,如果能够保障则输入的值则为 2000
        System.out.println("Result ="+resource.getNumber());
    }
}
@Data
class AtomicResource{AtomicInteger number = new AtomicInteger();
    public void addNum(){number.getAndIncrement();
    }
}

输入后果如下:

Result = 2000

面试官:你方才说到了 volatile 禁止指令重排,能够说说外面的原理吗?
此刻须要故作深思,须要体现出在回顾的样子,(为什么这么做,你懂得,毕竟没有面试官喜爱背题的同学)。
大黄:哦哦,这个之前操作理解过。计算机在底层执行程序的时候,为了提高效率,常常会对指令做重排序,个别重排序分为三种

  1. 编译器优化的重排序
  2. 指令并行的重排
  3. 内存零碎的重排

单线程下,无论怎么样重排序,最初执行的后果都统一的,并且指令重排遵循根本的数据依赖准则,数据须要先申明再计算;多线程下,线程交替执行,因为编译器存在优化重排,两个线程中应用的变量可能保障一致性是无奈确定的,后果无奈预测。

volatile 自身的原理是利用内存屏障来实现,通过插入内存屏障禁止在内存屏障前后的指令执行重排序的优化。
面试官:那内存屏障有啥作用呢,是怎么实现的呢?

大黄:

  1. 保障特定操作的执行程序
  2. 保障某些变量的内存可见性。

面试官:Volatile 与内存屏障又是如何起着作用的呢?

对于 Volatile 变量进行写操作时,会在写操作前面加上一个 store 屏障指令,将工作内存中的共享变量值即可刷新到主内存;
对于 Volatile 变量进行读操作时,会在读操作后面退出一个 load 屏障指令,读取之前马上读取主内存中的数据。

面试官心想:能够的,这个小伙子有点深度。我看看他是否用过。那你工作中在哪用到 volatile 了呢?

大黄:单例模式如果必须要在多线程下保障单例,volatile 关键字必不可少。

面试官:能够简略写一下一般的单例模式吗?

咱们先来看看一般的单例模式:

public class Juc004SingletonMultiThread {
    /**
     * 私有化构造方法、只会结构一次
     */
    private Juc004SingletonMultiThread(){System.out.println("构造方法");
    }
    private static Juc004SingletonMultiThread instance = null;
    public  static Juc004SingletonMultiThread getInstance(){if(instance == null){synchronized (Juc004SingletonMultiThread.class){if(instance == null){instance = new Juc004SingletonMultiThread();
                }
            }
        }
        return instance;
    }
    public static void main(String[] args) {
        // new 30 个线程,察看构造方法会创立几次
        for (int i = 0; i < 30; i++) {new Thread(()->{Juc004SingletonMultiThread.getInstance();
            },String.valueOf(i)).start();}
    }
}

大黄:留神哦,这曾经是极度强校验的单例模式了。然而这种双重查看的近线程平安的单例模式也有可能呈现问题,因为底层存在指令重排,查看的程序可能产生了变动,可能会产生读取到的 instance!=null,然而 instance 的援用对象可能没有实现初始化。,导致另一个线程读取到了还没有初始化的后果。

面试官:为什么会产生以上的状况呢?

大黄:这个可能须要从对象的初始化过程说起了。话说,盘古开天辟地…… 不好意思,跑题了,咱们持续。

 // step 1
public  static Juc004SingletonMultiThread getInstance(){                 
   // step 2
  if(instance == null){                                          
   // step 3
    synchronized (Juc004SingletonMultiThread.class){             
    // step 4
      if(instance == null){                                  
     // step 5
        instance = new Juc004SingletonMultiThread();}
    }
  }
  return instance;
}

第五步初始化过程会分为三步实现:

  1. 调配对象内存空间 memory = allocate()
  2. 初始化对象 instance(memory)
  3. 设置 instance 指向刚调配的内存地址,此时 instance = memory

再应用该初始化实现的对象,仿佛一起看起来是那么美妙,然而计算机底层编译器想着让你减速,则可能会自作聪明的将第三步和第二步调整程序(重排序),优化成了

  1. memory = allocate() 调配对象内存空间
  2. instance = memory 设置 instance 指向刚调配的内存地址,此时对象还没有哦
  3. instance(memory) 初始化对象

这种优化在单线程下还不要紧,因为第一次拜访该对象肯定是在这三步实现之后,然而多线程之间存在如此多的的竞争,如果有另一个线程在重排序之后的 3 前面拜访了该对象则有问题了,因为该对象基本就齐全初始化的。

面试官:好家伙,这个小伙子,必须要。能够简略画画拜访到图吗?

大黄拿起笔就绘制了如下图了:

多线程拜访内存模型

并且滔滔不绝到,然而上述问题在单线程下不存在该问题,只有波及到多线程下才会产生。
为了解决该问题能够从两个角度解决问题,

  1. 不容许 2 和 3 进行重排序
  2. 容许 2 和 3 重排序,然而不容许其余线程看到这个重排序。
    因而能够加上 Volatile 关键字避免指令重排。

面试官:那你写一下用 volatile 实现的单例模式吧

public class Juc004SingletonMultiThread {
    /**
     * 私有化构造方法、只会结构一次
     */
    private Juc004SingletonMultiThread(){System.out.println("构造方法");
    }
    private  static volatile Juc004SingletonMultiThread instance = null;
    public  static Juc004SingletonMultiThread getInstance(){if(instance == null){synchronized (Juc004SingletonMultiThread.class){if(instance == null){instance = new Juc004SingletonMultiThread();
                }
            }
        }
        return instance;
    }
    public static void main(String[] args) {
        // new 30 个线程,察看构造方法会创立几次
        for (int i = 0; i < 30; i++) {new Thread(()->{Juc004SingletonMultiThread.getInstance();
            },String.valueOf(i)).start();}
    }
}

面试到这里,我想面试官对于你的能力曾经不容置疑了。
面试官暗喜,嘿嘿,碰到宝了,好小子,有点货色啊,这种人才必须得拿下。

面试官:好了,明天的面试就到这里,请问你下一场面试什么时候有工夫呢,我来安顿一下。

哈哈哈,祝贺你,到了这里面试曾经胜利拿下了,收起你的笑容。

大黄:我这几天都有工夫的,看你们的安顿。

总结

自身次要围绕结尾的几个真正的面试题开展,简略来说,volatile是什么?为什么要有 volatilevolatile 底层原理?平时编程中哪里用到了volatile

最初大黄分享多年面试心得。面试中,面对一个问题,大略依照总分的逻辑答复即可。先间接抛出论断,而后举例论证本人的论断。肯定要第一工夫抓住面试官的心里,否则容易给人抓不着重点或者不着边际的印象。

番外

另外,关注大黄奔跑公众号,第一工夫播种独家整顿的面试实战记录及面试知识点总结。

我是大黄,一个只会写 HelloWorld 的程序员,咱们下期见。

关注大黄,充当 offer 收割机

正文完
 0