微信公众号:大黄奔跑
关注我,可了解更多有趣的面试相关问题。java
写在以前面试问题概览面试回顾大黄可见性Demo演示小插曲大黄可见性Demo演示小插曲大黄可见性Demo演示小插曲总结番外程序员
Hello,你们好,我是只会写HelloWorld的程序员大黄。面试
Java中并发编程是各个大厂面试重点,不少知识点晦涩难懂,经常须要结合实际经验才能回答好,面试没有回答好,则容易被面试官直接挂掉。编程
所以,大黄利用周末时间,呕心沥血,整理以前和面试官battle
的面试题目。安全
因为并发变成问题实在是太多了,一篇文章不足以囊括全部的并发知识点,打算分为多篇来分析,面试中的并发问题该如何回答。本篇主要围绕volatile
关键字展开。微信
关于并发编程一些源码和深层次的分析已经不胜枚举,大黄不打算从各方面展开,只但愿可以借用这篇文章沟通面试中该如何回答,毕竟面试时间短,回答重点、要点才是关键。 多线程
下面我罗列一些大厂面试中,关于并发编程常见的一些面试题目,有的是本身亲身经历,有的是寻找网友的面经分享。并发
能够先看看这些面试题目,如今心中想一想,若是你面对这些题目,该如何回答呢?app
- volatile关键字解释一下【字节跳动】
- volatile有啥做用,如何使用的呢【京东】
- synchronized 和volatile 关键字的区别【京东】
- volatile原理详细解释【阿里云】
- volatile关键字介绍,内存模型说一下【滴滴】
- Volatile底层原理,使用场景【抖音】
能够看到volatile
关键字在各个大厂面试中已经成为了必考的面试题目。回答好了必然称为加分项,回答很差嘿嘿,你懂的。测试
一个身着灰色格子衬衫,拿着闪着硕大的🍎logo小哥迎面走来,我心想,logo还自带发光的,这尼玛确定是p7大佬了,可是刚开始我们仍是得淡定不是。
面试官:大黄同窗是吧,我看你简历上面写可以熟练掌握并发编程核心知识,那咱们先来看看并发编程的一些核心知识吧。有听过volatile
吗?说说你对于这个的理解。
记住:此时仍是要从为何、是什么、有什么做用回答,只有这样才能给面试官留下深入印象。
大黄:面试官您好,volatile是java虚拟机提供的轻量级同步机制,主要特色有三个:
面试中,确定不是说完这三点就完了,通常须要展开来讲。
大黄:所谓可见性,是多线程中独有的。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线程没有办法立刻感知到变量的变化!!
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的值并非一步操做,过程能够分为三步:
每一个线程获取主内存中的值修改,而后再写回主内存,多个线程执行的时候,存在不少状况的写值的覆盖。
用下面的例子测试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
关键字修饰,前者的效率自己是后者高,不用加锁就能够保证原子性。
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
禁止指令重排,能够说说里面的原理吗?
此刻须要故做沉思,须要表现出在回忆的样子,(为何这么作,你懂得,毕竟没有面试官喜欢背题的同窗)。
大黄:哦哦,这个以前操做了解过。计算机在底层执行程序的时候,为了提升效率,常常会对指令作重排序,通常重排序分为三种
单线程下,不管怎么样重排序,最后执行的结果都一致的,而且指令重排遵循基本的数据依赖原则,数据须要先声明再计算;多线程下,线程交替执行,因为编译器存在优化重排,两个线程中使用的变量可以保证一致性是没法肯定的,结果没法预测。
volatile自己的原理是利用内存屏障来实现,经过插入内存屏障禁止在内存屏障先后的指令执行重排序的优化。
面试官:那内存屏障有啥做用呢,是怎么实现的呢?
大黄:
面试官: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;
}
第五步初始化过程会分为三步完成:
memory = allocate()
instance(memory)
instance = memory
再使用该初始化完成的对象,彷佛一块儿看起来是那么美好,可是计算机底层编译器想着让你加速,则可能会自做聪明的将第三步和第二步调整顺序(重排序),优化成了
- memory = allocate() 分配对象内存空间
- instance = memory 设置instance指向刚分配的内存地址,此时对象尚未哦
- instance(memory) 初始化对象
这种优化在单线程下还没关系,由于第一次访问该对象必定是在这三步完成以后,可是多线程之间存在如此多的的竞争,若是有另外一个线程在重排序以后的3后面访问了该对象则有问题了,由于该对象根本就彻底初始化的。
面试官:好家伙,这个小伙子,必需要。能够简单画画访问到图吗?
大黄拿起笔就绘制了以下图了:
面试官:那你写一下用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
是什么?为何要有volatile
?volatile
底层原理?平时编程中哪里用到了volatile
。
最后大黄分享多年面试心得。面试中,面对一个问题,大概按照总分的逻辑回答便可。先直接抛出结论,而后举例论证本身的结论。必定要第一时间抓住面试官的内心,不然容易给人抓不着重点或者不着边际的印象。
另外,关注大黄奔跑公众号,第一时间收获独家整理的面试实战记录及面试知识点总结。
我是大黄,一个只会写HelloWorld
的程序员,我们下期见。