1. 程式人生 > 其它 >成功入職位元組跳動!BATJ都愛問的Java多執行緒面試題整理

成功入職位元組跳動!BATJ都愛問的Java多執行緒面試題整理

成功入職位元組跳動!BATJ都愛問的Java多執行緒面試題整理

public static Singleton getUniqueInstance() {

   //先判斷物件是否已經例項過,沒有例項化過才進入加鎖程式碼

    if (uniqueInstance == null) {

        //類物件加鎖

        synchronized (Singleton.class) {

            if (uniqueInstance == null) {

                uniqueInstance = new Singleton();

            }

        }

    }

    return uniqueInstance;

}

}




另外,需要注意 uniqueInstance 採用 volatile 關鍵字修飾也是很有必要。



uniqueInstance 採用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段程式碼其實是分為三步執行:



1.  為 uniqueInstance 分配記憶體空間

2.  初始化 uniqueInstance

3.  將 uniqueInstance 指向分配的記憶體地址



但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單執行緒環境下不會出先問題,但是在多執行緒環境下會導致一個執行緒獲得還沒有初始化的例項。例如,執行緒 T1 執行了 1 和 3,此時 T2 呼叫 getUniqueInstance() 後發現 uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。



使用 volatile 可以禁止 JVM 的指令重排,保證在多執行緒環境下也能正常執行。



### 1.3 講一下 synchronized 關鍵字的底層原理



**synchronized 關鍵字底層原理屬於 JVM 層面。**



**① synchronized 同步語句塊的情況**



public class SynchronizedDemo {

public void method() {

	synchronized (this) {

		System.out.println("synchronized 程式碼塊");

	}

}

}




通過 JDK 自帶的 javap 命令檢視 SynchronizedDemo 類的相關位元組碼資訊:首先切換到類的對應目錄執行 `javac SynchronizedDemo.java` 命令生成編譯後的 .class 檔案,然後執行`javap -c -s -v -l SynchronizedDemo.class`。



![synchronized 關鍵字原理](https://user-gold-cdn.xitu.io/2018/10/26/166add616a292bcf?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)



從上面我們可以看出:



**synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步程式碼塊的開始位置,monitorexit 指令則指明同步程式碼塊的結束位置。** 當執行 monitorenter 指令時,執行緒試圖獲取鎖也就是獲取 monitor(monitor物件存在於每個Java物件的物件頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什麼Java中任意物件可以作為鎖的原因) 的持有權.當計數器為0則可以成功獲取,獲取後將鎖計數器設為1也就是加1。相應的在執行 monitorexit 指令後,將鎖計數器設為0,表明鎖被釋放。如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到鎖被另外一個執行緒釋放為止。



**② synchronized 修飾方法的的情況**



public class SynchronizedDemo2 {

public synchronized void method() {

	System.out.println("synchronized 方法");

}

}




![synchronized 關鍵字原理](https://user-gold-cdn.xitu.io/2018/10/26/166add6169fc206d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)



synchronized 修飾的方法並沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC\_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法,JVM 通過該 ACC\_SYNCHRONIZED 訪問標誌來辨別一個方法是否宣告為同步方法,從而執行相應的同步呼叫。



### 1.4 說說 JDK1.6 之後的synchronized 關鍵字底層做了哪些優化,可以詳細介紹一下這些優化嗎



JDK1.6 對鎖的實現引入了大量的優化,如偏向鎖、輕量級鎖、自旋鎖、適應性自旋鎖、鎖消除、鎖粗化等技術來減少鎖操作的開銷。



鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨著競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。



關於這幾種優化的詳細資訊可以檢視:[synchronized 關鍵字使用、底層原理、JDK1.6 之後的底層優化以及 和ReenTrantLock 的對比](https://gitee.com/vip204888/java-p7)



### 1.5 談談 synchronized和ReenTrantLock 的區別



**① 兩者都是可重入鎖**



兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內部鎖。比如一個執行緒獲得了某個物件的鎖,此時這個物件鎖還沒有釋放,當其再次想要獲取這個物件的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個執行緒每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖。



**② synchronized 依賴於 JVM 而 ReenTrantLock 依賴於 API**



synchronized 是依賴於 JVM 實現的,前面我們也講到了 虛擬機器團隊在 JDK1.6 為 synchronized 關鍵字進行了很多優化,但是這些優化都是在虛擬機器層面實現的,並沒有直接暴露給我們。ReenTrantLock 是 JDK 層面實現的(也就是 API 層面,需要 lock() 和 unlock 方法配合 try/finally 語句塊來完成),所以我們可以通過檢視它的原始碼,來看它是如何實現的。



**③ ReenTrantLock 比 synchronized 增加了一些高階功能**



相比synchronized,ReenTrantLock增加了一些高階功能。主要來說主要有三點:**①等待可中斷;②可實現公平鎖;③可實現選擇性通知(鎖可以繫結多個條件)**



*   **ReenTrantLock提供了一種能夠中斷等待鎖的執行緒的機制**,通過lock.lockInterruptibly()來實現這個機制。也就是說正在等待的執行緒可以選擇放棄等待,改為處理其他事情。

*   **ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的執行緒先獲得鎖。** ReenTrantLock預設情況是非公平的,可以通過 ReenTrantLock類的`ReentrantLock(boolean fair)`構造方法來制定是否是公平的。

*   synchronized關鍵字與wait()和notify/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock類當然也可以實現,但是需要藉助於Condition介面與newCondition() 方法。Condition是JDK1.5之後才有的,它具有很好的靈活性,比如可以實現多路通知功能也就是在一個Lock物件中可以建立多個Condition例項(即物件監視器),**執行緒物件可以註冊在指定的Condition中,從而可以有選擇性的進行執行緒通知,在排程執行緒上更加靈活。 在使用notify/notifyAll()方法進行通知時,被通知的執行緒是由 JVM 選擇的,用ReentrantLock類結合Condition例項可以實現“選擇性通知”** ,這個功能非常重要,而且是Condition介面預設提供的。而synchronized關鍵字就相當於整個Lock物件中只有一個Condition例項,所有的執行緒都註冊在它一個身上。如果執行notifyAll()方法的話就會通知所有處於等待狀態的執行緒這樣會造成很大的效率問題,而Condition例項的signalAll()方法 只會喚醒註冊在該Condition例項中的所有等待執行緒。



如果你想使用上述功能,那麼選擇ReenTrantLock是一個不錯的選擇。



**④ 效能已不是選擇標準**



二、面試中關於執行緒池的 4 連擊

================



### 2.1 講一下Java記憶體模型



在 JDK1.2 之前,Java的記憶體模型實現總是從**主存**(即共享記憶體)讀取變數,是不需要進行特別的注意的。而在當前的 Java 記憶體模型下,執行緒可以把變數儲存**本地記憶體**(比如機器的暫存器)中,而不是直接在主存中進行讀寫。這就可能造成一個執行緒在主存中修改了一個變數的值,而另外一個執行緒還繼續使用它在暫存器中的變數值的拷貝,造成**資料的不一致**。



![資料的不一致](https://user-gold-cdn.xitu.io/2018/10/30/166c46ede4423ba2?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)



要解決這個問題,就需要把變數宣告為 **volatile**,這就指示 JVM,這個變數是不穩定的,每次使用它都到主存中進行讀取。



說白了, **volatile** 關鍵字的主要作用就是保證變數的可見性然後還有一個作用是防止指令重排序。



![volatile關鍵字的可見性](https://user-gold-cdn.xitu.io/2018/10/30/166c46ede4b9f501?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)



### 2.2 說說 synchronized 關鍵字和 volatile 關鍵字的區別



synchronized關鍵字和volatile關鍵字比較



*   **volatile關鍵字**是執行緒同步的**輕量級實現**,所以**volatile效能肯定比synchronized關鍵字要好**。但是**volatile關鍵字只能用於變數而synchronized關鍵字可以修飾方法以及程式碼塊**。synchronized關鍵字在JavaSE1.6之後進行了主要包括為了減少獲得鎖和釋放鎖帶來的效能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之後執行效率有了顯著提升,**實際開發中使用 synchronized 關鍵字的場景還是更多一些**。

*   **多執行緒訪問volatile關鍵字不會發生阻塞,而synchronized關鍵字可能會發生阻塞**

*   **volatile關鍵字能保證資料的可見性,但不能保證資料的原子性。synchronized關鍵字兩者都能保證。**

*   **volatile關鍵字主要用於解決變數在多個執行緒之間的可見性,而 synchronized關鍵字解決的是多個執行緒之間訪問資源的同步性。**



三、面試中關於 執行緒池的 2 連擊

=================



### 3.1 為什麼要用執行緒池?



執行緒池提供了一種限制和管理資源(包括執行一個任務)。 每個執行緒池還維護一些基本統計資訊,例如已完成任務的數量。



這裡借用《Java併發程式設計的藝術》提到的來說一下使用執行緒池的好處:



*   **降低資源消耗。** 通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。

*   **提高響應速度。** 當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。

*   **提高執行緒的可管理性。** 執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。



### 3.2 實現Runnable介面和Callable介面的區別



如果想讓執行緒池執行任務的話需要實現的Runnable介面或Callable介面。 Runnable介面或Callable介面實現類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行。兩者的區別在於 Runnable 介面不會返回結果但是 Callable 介面可以返回結果。



**備註:** 工具類`Executors`可以實現`Runnable`物件和`Callable`物件之間的相互轉換。(`Executors.callable(Runnable task)`或`Executors.callable(Runnable task,Object resule)`)。



### 3.3 執行execute()方法和submit()方法的區別是什麼呢?



1)**`execute()` 方法用於提交不需要返回值的任務,所以無法判斷任務是否被執行緒池執行成功與否;**



2)**submit()方法用於提交需要返回值的任務。執行緒池會返回一個future型別的物件,通過這個future物件可以判斷任務是否執行成功**,並且可以通過future的get()方法來獲取返回值,get()方法會阻塞當前執行緒直到任務完成,而使用 `get(long timeout,TimeUnit unit)`方法則會阻塞當前執行緒一段時間後立即返回,這時候有可能任務沒有執行完。



### 3.4 如何建立執行緒池



《阿里巴巴Java開發手冊》中強制執行緒池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險\*\*



> Executors 返回執行緒池物件的弊端如下:

> 

> *   **FixedThreadPool 和 SingleThreadExecutor** : 允許請求的佇列長度為 Integer.MAX\_VALUE,可能堆積大量的請求,從而導致OOM。

> *   **CachedThreadPool 和 ScheduledThreadPool** : 允許建立的執行緒數量為 Integer.MAX\_VALUE ,可能會建立大量執行緒,從而導致OOM。



**方式一:通過構造方法實現**



![通過構造方法實現](https://user-gold-cdn.xitu.io/2018/10/30/166c4a5baac923e9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)



**方式二:通過Executor 框架的工具類Executors來實現** 我們可以建立三種類型的ThreadPoolExecutor:



*   **FixedThreadPool** : 該方法返回一個固定執行緒數量的執行緒池。該執行緒池中的執行緒數量始終不變。當有一個新的任務提交時,執行緒池中若有空閒執行緒,則立即執行。若沒有,則新的任務會被暫存在一個任務佇列中,待有執行緒空閒時,便處理在任務佇列中的任務。

*   **SingleThreadExecutor:** 方法返回一個只有一個執行緒的執行緒池。若多餘一個任務被提交到該執行緒池,任務會被儲存在一個任務佇列中,待執行緒空閒,按先入先出的順序執行佇列中的任務。

*   **CachedThreadPool:** 該方法返回一個可根據實際情況調整執行緒數量的執行緒池。執行緒池的執行緒數量不確定,但若有空閒執行緒可以複用,則會優先使用可複用的執行緒。若所有執行緒均在工作,又有新的任務提交,則會建立新的執行緒處理任務。所有執行緒在當前任務執行完畢後,將返回執行緒池進行復用。



對應Executors工具類中的方法如圖所示:



![通過Executor 框架的工具類Executors來實現](https://user-gold-cdn.xitu.io/2018/10/30/166c4a5baa9ca5e9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)



四、面試中關於 Atomic 原子類的 4 連擊

========================



### 4.1 介紹一下Atomic 原子類



Atomic 翻譯成中文是原子的意思。在化學上,我們知道原子是構成一般物質的最小單位,在化學反應中是不可分割的。在我們這裡 Atomic 是指一個操作是不可中斷的。即使是在多個執行緒一起執行的時候,一個操作一旦開始,就不會被其他執行緒干擾。



所以,所謂原子類說簡單點就是具有原子/原子操作特徵的類。



併發包 `java.util.concurrent` 的原子類都存放在`java.util.concurrent.atomic`下,如下圖所示。



![JUC 原子類概覽](https://user-gold-cdn.xitu.io/2018/10/30/166c4ac08d4c5547?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)



### 4.2 JUC 包中的原子類是哪4類?



**基本型別**



使用原子的方式更新基本型別



*   AtomicInteger:整形原子類

*   AtomicLong:長整型原子類

*   AtomicBoolean :布林型原子類



**陣列型別**



使用原子的方式更新數組裡的某個元素



*   AtomicIntegerArray:整形陣列原子類

*   AtomicLongArray:長整形陣列原子類

*   AtomicReferenceArray :引用型別陣列原子類



**引用型別**



*   AtomicReference:引用型別原子類

*   AtomicStampedRerence:原子更新引用型別裡的欄位原子類

*   AtomicMarkableReference :原子更新帶有標記位的引用型別



**物件的屬性修改型別**



*   AtomicIntegerFieldUpdater:原子更新整形欄位的更新器

*   AtomicLongFieldUpdater:原子更新長整形欄位的更新器

*   AtomicStampedReference :原子更新帶有版本號的引用型別。該類將整數值與引用關聯起來,可用於解決原子的更新資料和資料的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。



### 4.3 講講 AtomicInteger 的使用



**AtomicInteger 類常用方法**



public final int get() //獲取當前的值

public final int getAndSet(int newValue)//獲取當前的值,並設定新的值

public final int getAndIncrement()//獲取當前的值,並自增

public final int getAndDecrement() //獲取當前的值,並自減

public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值

boolean compareAndSet(int expect, int update) //如果輸入的數值等於預期值,則以原子方式將該值設定為輸入值(update)

public final void lazySet(int newValue)//最終設定為newValue,使用 lazySet 設定之後可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。




**AtomicInteger 類的使用示例**



使用 AtomicInteger 之後,不用對 increment() 方法加鎖也可以保證執行緒安全。



class AtomicIntegerTest {

    private AtomicInteger count = new AtomicInteger();

  //使用AtomicInteger之後,不需要對該方法加鎖,也可以實現執行緒安全。

    public void increment() {

              count.incrementAndGet();

    }

 

   public int getCount() {

            return count.get();

    }

}




### 4.4 能不能給我簡單介紹一下 AtomicInteger 類的原理



AtomicInteger 執行緒安全原理簡單分析



AtomicInteger 類的部分原始碼:





# 總結

我個人認為,如果你想靠著背面試題來獲得心儀的offer,用癩蛤蟆想吃天鵝肉形容完全不過分。想必大家能感受到面試越來越難,想找到心儀的工作也是越來越難,高薪工作羨慕不來,卻又對自己目前的薪資不太滿意,工作幾年甚至連一個應屆生的薪資都比不上,終究是錯付了,錯付了自己沒有去提升技術。

這些面試題分享給大家的目的,其實是希望大家通過大廠面試題分析自己的技術棧,給自己梳理一個更加明確的學習方向,當你準備好去面試大廠,你心裡有底,大概知道面試官會問多廣,多深,避免面試的時候一問三不知。

大家可以把Java基礎,JVM,併發程式設計,MySQL,Redis,Spring,Spring cloud等等做一個知識總結以及延伸,再去進行操作,不然光記是學不會的,這裡我也提供一些腦圖分享給大家:

![](https://upload-images.jianshu.io/upload_images/22932333-fff8d1bc09986cf8?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![](https://upload-images.jianshu.io/upload_images/22932333-b886365cdb009d9d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![](https://upload-images.jianshu.io/upload_images/22932333-cb4ad5410078e845?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

希望你看完這篇文章後,不要猶豫,抓緊學習,複習知識,準備在明年的金三銀四拿到心儀的offer,加油,打工人!

**[領取資料只需要點選這裡即可免費獲取全部資料!](https://gitee.com/vip204888/java-p7)**