微信公衆號:大黃奔跑
關注我,可瞭解更多有趣的面試相關問題。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
的程序員,我們下期見。