微信公众号:大黄奔跑
关注我,可了解更多有趣的面试相关问题。

写在之前面试问题概览面试回顾大黄可见性Demo演示小插曲大黄可见性Demo演示小插曲大黄可见性Demo演示小插曲总结番外

写在之前

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收割机

Offer快到碗里来,Volatile问题终结者的更多相关文章

  1. Bug快到碗里来

    Bug快到碗里来 python错误--'list' object is not callable 原因及解决方法1 你定义了一个变量的变量名和系统自带的关键字冲突,调用变量时关键字被传到调用的位置,就 ...

  2. [剑指Offer]快排

    快排 看到一篇博文提到"东拆西补"的思想,非常贴切了. 这里采用传统的方法,没有采用剑指Offer书上的方法. 细节很多,需巩固. 其他知识点 生成一个范围内随机数 见代码,这里为 ...

  3. 剑指Offer——线程同步volatile与synchronized详解

    (转)Java面试--线程同步volatile与synchronized详解 0. 前言 面试时很可能遇到这样一个问题:使用volatile修饰int型变量i,多个线程同时进行i++操作,这样可以实现 ...

  4. 这里有一篇简单易懂的webSocket 快到碗里来~

    这篇文章是我在学习的时候看到的  刚开始还不是很理解  后来自己百度 又问了一些人  回过头在看这篇文章 真的挺好的 但是原创已经不知道是谁了  转载哦~~~ -------------------- ...

  5. JS判断图片是否加载完成 背景图404 快到碗里来

    面对这个问题 我最多做到表面笑嘻嘻 …… 真不知道测试怎么那么…… 啥都能给你测出来 有的没的都能给你测出来 算了算了  谁让本仙女本精灵本可爱温柔大方善解人意呢 …呵呵呵 ————————————正 ...

  6. ES6快到碗里来---一个简单的爬虫指南

    学习ES6的时候,没少看ES6入门,到现在也就明白了个大概(惭愧脸).这里不谈ES6,只谈怎么把ES6的页面爬下来放到一起成为一个离线文档. 之前居然没注意过作者把这本书开源了..瞎耽误功夫...地址 ...

  7. Something about SeekingJob---Resume简历

    这几天脑子里满满的装的都是offer.offer.offer快到碗里来,但是offer始终不是巧克力,并没那么甜美可口易消化. 找工作刚开始,就遇到了不小的阻力,看到Boss直聘上各种与IT相关的工作 ...

  8. 2016阿里巴巴校招offer面经

    前段时间参加阿里巴巴校招,非常荣幸,很快就拿到了offer,经历了三轮技术面试和一轮hr面,面试官们都非常nice,在此感谢一下各位面试官,你们辛苦了,百忙之中抽时间面试!为了帮助更多人想进阿里巴巴的 ...

  9. [转]CocoaPods安装和使用教程

    转载地址:http://code4app.com/article/cocoapods-install-usage 目录 CocoaPods是什么? 如何下载和安装CocoaPods? 如何使用Coco ...

随机推荐

  1. 自动化运维工具之Puppet常用资源(一)

    前文我们聊到了puppet的架构,单机模型和master/agent模型的工作流程以及puppet的基础使用,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/14 ...

  2. KNN 算法-理论篇-如何给电影进行分类

    公号:码农充电站pro 主页:https://codeshellme.github.io KNN 算法的全称是K-Nearest Neighbor,中文为K 近邻算法,它是基于距离的一种算法,简单有效 ...

  3. 【NOIP2017提高A组模拟9.7】JZOJ 计数题

    [NOIP2017提高A组模拟9.7]JZOJ 计数题 题目 Description Input Output Sample Input 5 2 2 3 4 5 Sample Output 8 6 D ...

  4. Python中动态编译函数compile(source, filename, mode, ......)参数filename的作用是什么?

    动态编译函数compile调用语法如下: compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1) 其中的fi ...

  5. 老猿学5G扫盲贴:NEF、NRF、AF、UPF以及DN的功能

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 NEF:Network Exposure Function ,网络开放 ...

  6. 老猿学5G随笔:5G系统构成

    5G系统(5G智能计费方案)简称5GS,由以下部分组成: 用户设备:User Equipment,简写UE,用户访问网络的设备 5G接入网:5G Access NetWork,简写为5G-AN,负责用 ...

  7. Python中高级知识(非专题部分)学习随笔

    Python学习随笔:使用xlwings读取和操作Execl文件 Python学习随笔:使用xlwings新建Execl文件和sheet的方法 博客地址:https://blog.csdn.net/L ...

  8. 谈谈传说中的redo log是什么?有啥用?

    目录 一.引出 redo log 的作用 二.思考一个问题: 三.redo log block 四.redo log buffer 五.redo log的刷盘时机 六.推荐参数 七.redo log ...

  9. 小程序image无法显示图片

    图片路径中有中文 图片地址为http开头,图片只能在调试模式中显示,真机也必须开调试. 图片名称有空格 图片的后缀必须为小写的.png或者.jpg

  10. 【坑点集合】C++ STL 使用注意事项整理

    Intro 简单整理了一些关于 C++ STL 的注意点. 虽然大多数东西可以手写不过某些东西最好少造轮子,善用 STL 可以节约很多考场时间,简化实现. 当然是时空限制和功能足够的前提下. Tips ...