JUC面試點彙總

秋落雨微涼發表於2022-12-03

JUC面試點彙總

我們會在這裡介紹我所涉及到的JUC相關的面試點內容,本篇內容持續更新

我們會介紹下述JUC的相關面試點:

  • 執行緒狀態
  • 執行緒池
  • Wait和Sleep
  • Synchronized和Lock
  • Volatile執行緒安全
  • 悲觀鎖和樂觀鎖
  • Hashtable和ConcurrentHashMap
  • ThreadLocal

執行緒狀態

下面我們來介紹我們面試中經常考察的兩種執行緒狀態分類

六種執行緒狀態

Java虛擬機器將執行緒狀態劃分為六種:

我們來簡單介紹一下:

  • NEW 執行緒剛被建立,但是還沒有呼叫 start() 方法
  • RUNNABLE 當呼叫了 start() 方法之後
  • 注意,Java API 層面的 RUNNABLE 狀態涵蓋了作業系統層面的可執行狀態執行狀態和阻塞狀態
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 層面對阻塞狀態的細分
  • BLOCKED 表示被鎖攔截下的阻塞
  • WAITING 表示自己進行wait等待時的阻塞
  • TIMED_WAITING 表示進行有時間限制的阻塞
  • TERMINATED 當執行緒程式碼執行結束

五種執行緒狀態

作業系統將執行緒分為五種狀態:

我們來簡單介紹一下:

  • 【初始狀態】僅是在語言層面建立了執行緒物件,還未與作業系統執行緒關聯
  • 【可執行狀態】(就緒狀態)指該執行緒已經被建立(與作業系統執行緒關聯),可以由 CPU 排程執行
  • 【執行狀態】指獲取了 CPU 時間片執行中的狀態
    • 當 CPU 時間片用完,會從【執行狀態】轉換至【可執行狀態】,會導致執行緒的上下文切換
  • 【阻塞狀態】
    • 如果呼叫了阻塞 API,如 BIO 讀寫檔案,這時該執行緒實際不會用到 CPU,會導致執行緒上下文切換,進入 【阻塞狀態】
    • 等 BIO 操作完畢,會由作業系統喚醒阻塞的執行緒,轉換至【可執行狀態】
    • 與【可執行狀態】的區別是,對【阻塞狀態】的執行緒來說只要它們一直不喚醒,排程器就一直不會考慮 排程它們
  • 【終止狀態】表示執行緒已經執行完畢,生命週期已經結束,不會再轉換為其它狀態

執行緒池

下面我們來介紹執行緒池中常考的一些知識點

執行緒池工作流程圖

首先我們給出執行緒池的工作流程圖以及相關引數:

我們對上面元素進行簡單介紹:

  • submit(task):負責分配任務,將任務傳入WorkQueue
  • WorkQueue:工作等待佇列,用於存放未被執行的任務,通常具有任務個數限制,防止記憶體過滿
  • 核心執行緒:一直處於執行狀態的執行緒,不斷從WorkQueue中取得任務並執行
  • 救濟執行緒:只有當核心執行緒全部執行且WorkQueue裝載滿員並且有submit繼續傳入任務時開啟,在一定時間沒有任務接收後結束

執行緒池工作引數

我們給出執行緒池工作的基本引數:

/*corePoolSize核心執行緒數目*/

用於控制核心執行緒的個數
    
/*maximumPoolSize最大執行緒數目*/
    
表示核心執行緒和救急執行緒的最大個數,用該值減去corePoolSize核心執行緒數目就是救急執行緒個數
    
/*keepAliveTime生存時間*/
    
針對救急執行緒,當救急執行緒在該時間段沒有接收新任務,就結束該執行緒
    
/*unit時間單位*/
    
配合keepAliveTime生存時間使用的時間單位
    
/*workQueue阻塞佇列*/
    
用於存放處於阻塞狀態的任務
    
/*threadFactory執行緒工廠*/
    
用於生成執行緒名稱
    
/*handler拒絕策略*/
    
當執行緒均處於執行狀態,workQueue滿員,且有新任務進入時,handler負責處理新進入的執行緒
    
- AbortPolicy 讓呼叫者丟擲 RejectedExecutionException 異常,這是預設策略
- CallerRunsPolicy 讓呼叫者執行任務 
- DiscardPolicy 放棄本次任務 
- DiscardOldestPolicy 放棄佇列中最早的任務,本任務取而代之 
- Dubbo 的實現,在丟擲 RejectedExecutionException 異常之前會記錄日誌,並 dump 執行緒棧資訊,方 便定位問題 
- Netty 的實現,是建立一個新執行緒來執行任務 
- ActiveMQ 的實現,帶超時等待(60s)嘗試放入佇列,類似我們之前自定義的拒絕策略 
- PinPoint 的實現,它使用了一個拒絕策略鏈,會逐一嘗試策略鏈中每種拒絕策略

執行緒池構建程式碼

我們直接給出執行緒池構建程式碼,瞭解即可:

/*執行緒池構造:我之前有執行緒池專門的文章,可以深入瞭解*/

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

Wait和Sleep

我們來介紹一下wait和sleep的相關面試點

Wait和Sleep區別以及共同點

我們給出wait和sleep的區別以及共同點:

/*共同點*/

wait(),wait(long),sleep(long) 都會導致阻塞,將當前執行緒暫時放棄CPU使用權
    
/*不同點*/
    
// 方法歸屬類不同
wait 	屬於	Object實體類
sleep 	屬於	Thread執行緒類
    
// 醒來時機不同
wait():			只有當notify喚醒時才會醒來
wait(long): 	當時間結束,或者notify喚醒就會醒來
sleep(long):	當時間結束才會醒來
    
注意:都可以被打斷喚醒!
    
// 鎖性質不同
1.wait必須配合鎖一同使用,且只有鎖物件呼叫Lock.lock;sleep在任何時間段都可以使用
2.wait在lock時會解除當前鎖的限制;sleep若在鎖中不會解除當前鎖的限制

Synchronized和Lock

我們來介紹一下Synchronized和Lock的相關面試點

Synchronized和Lock區別以及共同點

我們會從三個層面講解兩者的區別以及共同點:

/*語法層面*/

// 所屬類
synchronized:關鍵字,屬於JVM,用C++語言實現
Lock:介面,屬於JDK,用Java語言實現
    
// 鎖實現
synchronized:在結束同步程式碼塊後自動釋放鎖
Lock:在結束程式碼塊後,需要手動unlock釋放鎖
    
/*功能層面*/
    
// 相同點
均屬於悲觀鎖,具備互斥,同步,鎖重入功能
    
// 不同點
Lock提供了synchronized所不具備的功能:獲得等待狀態,公平鎖,可打斷,可超時,多條件變數
Lock提供了多場景Lock:ReentrantLock,ReentrantReadWriteLock
    
/*效能方面*/

// 效能差距
synchronized:在無競爭情況下,存在輕量級鎖,偏向鎖,效能較高
Lock:在競爭激烈的情況下,效能更好

相關知識點補充

我們來補充上述所講述的部分知識點:

/*知識點補充*/

// Lock正常使用

owner:正在執行程式,存有status,表示幾重鎖,當鎖重入時,status++;解除鎖時,status--;當status==0,釋放鎖
blocked queue:阻塞佇列
waiting queue:等待佇列
    
// 公平鎖和非公平鎖
    
公平鎖:所有任務在進入時均放於阻塞佇列按順序排序並執行
非公平鎖:當owner釋放鎖後,新加入的任務可以和阻塞佇列的任務處於同一級競爭鎖
    
// 多條件變數
條件變數建立:Condition c1 = Lock.newCondition("c1");
條件變數使用:c1.await();
條件變數喚醒:c1.signal();
條件變數喚醒:c1.signalAll();

必須處於owner才能使用await,喚醒後放於Blocked Queue尾部等待

Volatile執行緒安全

我們來介紹一下Volatile的執行緒安全相關問題

Volatile執行緒安全

我們首先要知道執行緒安全主要從三方面解釋:

/*可見性*/

當前執行緒對數值的修改是否對其他執行緒可見?
    
問題產生原因:
    CPU和記憶體之間還有一層快取區,當使用資料較多時,會直接將記憶體的資料放入快取區,然後直接從快取區調入資料
    這時倘若另一個執行緒修改了記憶體中的資料,但是原執行緒仍舊從自己的快取區讀取資料,就會導致資料不可見
    
/*有序性*/
    
當前執行緒內的程式碼是否按照編寫順序執行?
    
問題產生原因:
    JVM存在自動編寫機制,當出現同級別的程式碼時,JVM會自動最佳化程式碼順序,加快速度,可能導致程式碼執行順序錯亂
    
/*原子性*/
    
當前執行緒內的程式碼是否為一次執行?
    
問題產生原因:
    執行緒是由CPU排程使用的,倘若執行緒排程到達指定時間片,可能就會導致執行緒內程式碼未完成執行而被其他執行緒使用的情況

然後我們需要知道Volatile對這三種特性的可控度:

/*可見性*/

Volatile可以保證資料的可見性

Volatile會使該屬性的每次讀取都預設從記憶體中讀取(該屬性不會被存放於緩衝區)
    
/*有序性*/
    
Volatile可以保證資料的有序性
    
Volatile存在寫屏障和讀屏障
    寫屏障出現在屬性輸入之後,在寫屏障之前的程式碼順序不會變更
    讀屏障出現在屬性讀取之前,在讀屏障之後的程式碼順序不會變更
    
/*原子性*/
    
Volatile無法保證資料的原子性!

悲觀鎖和樂觀鎖

我們來介紹一下悲觀鎖和樂觀鎖的面試點

悲觀鎖和樂觀鎖區別

我們來介紹一下悲觀鎖和樂觀鎖的區別:

/*悲觀鎖*/

代表:
    Synchronized
    Lock
    
特點:
    1.核心思想:當前執行緒佔用鎖後,才能操作共享資料,只有噹噹前執行緒結束操作後,其他執行緒才能競爭
    2.執行緒的執行與阻塞都會導致上下文切換,頻繁的上下文切換會導致CPU速度降低
    3.悲觀鎖大部分都存在自旋現象,在獲得鎖時會多次嘗試來減少上下文切換次數
    
/*樂觀鎖*/
    
代表:
    Atomic系列
    AtomicInteger
    
特點:
	1.核心思想:所有資料都可以操作共享資料,但只有一個執行緒可以修改資料,其他執行緒如果修改失敗就會不斷嘗試
    2.由於執行緒一直執行,不需要阻塞,不涉及上下文切換
    3.由於執行緒一直執行,需要一個CPU保證其執行緒的活動,否則單CPU下樂觀鎖屬於負增益,一般執行緒數不會超過CPU個數

悲觀鎖樂觀鎖程式碼比較

我們分別給出悲觀鎖和樂觀鎖的程式碼展示:

/*樂觀鎖底層實現*/

// 樂觀鎖底層其實是採用Unsafe類來完成的

// 1.獲得該類的屬性對於類的偏移量(第一個引數:類名稱.class,第二個引數:類屬性名稱)
Long BALANCE = unsafe.objectFieldOffset(Account.class,"blance");

// 2.採用unsafe的原子比較賦值方法(第一個引數:類物件,第二個引數:屬性偏移量,第三個引數:修改前數值,第四個引數:修改後數值)
// 如果再次檢測時,該值和oldInt相同,就將其修改為newInt,否則不作為
unsafe.compareAndSetInt(account,BALANCE,oldInt,newInt);

/*樂觀鎖程式碼*/

Thread t1 = new Thread(() -> {
    // 樂觀鎖需要不斷嘗試
    while(true){
        // 每次獲得當前值
        int oldInt = account.getBalance;
        // 我們假設做++操作
        int newInt = oldInt + 1;
        // 然後啟動unsafe的比較賦值(compareAndSetInt會返回一個布林值表示是否成功),若成功退出迴圈
        if(unsafe.compareAndSetInt(account,BALANCE,oldInt,newInt)){
            break;
        }
    }
}).start;

/*悲觀鎖程式碼*/

Thread t2 = new Thread(() -> {
    // 悲觀鎖就是直接採用鎖處理即可
    synchronized(Account.class){
        int oldInt = account.getBalance;
        int newInt = oldInt + 1;
        account.setBalance(newInt);
    }
})

Hashtable和ConcurrentHashmap

我們來介紹一下Hashtable和ConcurrentHashmap的面試點

Hashtable和ConcurrentHashmap區別

我們來介紹一下Hashtable和ConcurrentHashmap區別:

/*執行緒安全?*/

Hashtable和ConcurrentHashMap均屬於執行緒安全類的Map集合
    
/*併發度*/
    
Hashtable:只存在一個鎖,所有索引點的操作均在一個鎖上,併發度低
    
1.7ConcurrentHashMap:底層由陣列+Segment+連結串列結構,每個Segment對應一把鎖,不同Segment不會造成鎖衝突
    
1.8ConcurrentHashMap:底層由陣列+連結串列結構,每個連結串列頭對應一把鎖,相當於每個索引點對應一把鎖,只有同一條連結串列會產生鎖衝突

Hashtable

我們來介紹一下Hashtable的基本面試點:

/*基本問題*/

初始capacity:11
    
擴容:超過0.75
    
索引:hashcode即可,(因為以質數為主,分散性較好,不需要二次hash)

1.7ConcurrentHashMap

我們來介紹一下JDK1.7版本的ConcurrentHashMap:

/*基本組成*/

capacity:總共的索引頭
    
factor:超過0.75
    
clevel:併發度,也就是Segment的個數
    
每個Segment算是一個大桶,然後大桶中會根據capacity/clevel算出小桶
    
舉例:
    capacity:32
    factor:0.75
    clevel:8
    
    這時存在8個Segment,每個Segment中存有四個初始小桶
    
    不同Segment擁有不同鎖,不同Segment獨自佔有併發性
    
    基本形式如下:
    ---- ---- ---- ---- ---- ---- ---- ----
    
/*put操作*/
    
1.hashCode
    
2.hash
    
3.根據hash值二進位制的前(clevel二進位制為2的n次方)n位的數值來判斷放在哪個大桶
    
4.根據hash值二進位制的後(小桶大小二進位制為2的n次方)n為的數值來判斷放哪個大桶
    
舉例:
    capacity:32
    factor:0.75
    clevel:8
    
    首先clevel的為2的3次方,小桶大小為2的2次方
    
    假設我們的hash值二進位制為 110 1101 0110

    這時我們的大桶取前三位:110 -> 6 -> Segment[6]
    
    這時我們的小桶取後兩位: 10 -> 2 -> Segment[6][2]
    
    也就是第七個桶的第三個位置
    
/*擴容*/
    
擴容僅針對每個Segment單獨擴容,最開始的桶大小為capacity/clevel,當超過factor,就會自動擴大一倍,單獨計算
    
Segment[0]的擴容不會影響到其他Segment的桶大小

/*Segment[0]*/
    
Segment[0]會自動初始化小桶,其他Segment只有在put第一個數時初始化
    
因為Segment[0]類似於一個初始模板,其他Segment會根據Segment[0]的大小來構造,節省空間(懶漢式構建)

1.8ConcurrentHashMap

我們來介紹一下JDK1.8版本的ConcurrentHashMap:

/*基本知識點*/

1.8版本的ConcurrentHashMap只包含 陣列 + 連結串列 結構
    
屬於懶漢式初始化,我們new一個ConcurrentHashMap並不會產生陣列,只有開始put時才會初始化
    
capacity:16
	注意:這裡的capacity並不是初始桶大小,而是我們需要插入的數的數量,系統會根據我們書寫的capacity更換桶大小
    例如我們寫15,系統會為我們分配一個大小為32的桶
    
factor:達到0.75

/*併發依據*/
    
1.8ConcurrentHashMap根據連結串列頭分配不同的鎖,也就是如果不是在同一索引下,均可以正常執行
    
/*擴容操作*/
    
擴容是ConcurrentHashMap的考點之一
    
ConcurrenthashMap擴容是從後往前移動資料,每次移動完成該索引點資料,就為其標記為ForwardingNode用來表示已移動
    
ConcurrentHashMap擴容不再是將原資料next更換,而是直接在新陣列上建立新資料,將資料複製過去,防止併發操作時出現問題
    
/*擴容細節*/
    
當一個執行緒t1正在進行擴容,另一個執行緒t2參與該HashMap各項操作:
    
1.get操作:
	I.如果查詢的該索引屬於ForwardingNode,就去新的陣列中查詢
    II.如果查詢的索引不屬於ForwardingNode,就直接查詢
    
2.put操作:
    I.如果該索引不屬於ForwardingNode,就直接插入即可
    II.如果該索引正在遷移,堵塞
    III.如果該索引已經屬於ForwardingNode,幫助執行緒t1完成擴容後,再進行修改

ThreadLocal

我們來介紹一下ThreadLocal的面試點

對ThreadLocal的理解

我們來講解一下對對ThreadLocal的理解:

/*執行緒安全性*/

ThreadLocal可以實現資源物件的執行緒隔離,讓每個執行緒各用各的資源物件,避免爭用引發的執行緒安全問題
    
ThreadLocal同時實現了執行緒內資源共享
    
/*ThreadLocal使用*/
    
每個執行緒中有一個ThreadLocalMap型別的成員變數,用於儲存資源物件
    
1.建立一個ThreadLocal(該ThreadLocal實際上就是ThreadLocalMap的key)
    ThreadLocal<class?> t1 = new ThreadLocal<>();

2.可以呼叫set方法儲存資料(ThreadLocal就是key,資源物件作為value,放入當前執行緒的ThreadLocalMap集合中)
	t1.set(new String("123"));

3.可以呼叫get方法查詢資料(根據ThreadLocal為key,查詢相關資料)
    t1.get();

4.可以呼叫remove方法刪除該資料(根據ThreadLocal為key,刪除相關資料)
    
注意點:
    I.當使用set時才會構造map物件
    II.不同的ThreadLocal使用不同的資料結構
    III.擴容時閾值為0.75,每次擴容一倍;儲存時採用開放定址法
    
/*key*/
    
這裡的key採用的是弱引用:
    1.Thread可能需要長時間執行(如執行緒池的執行緒),如果key不再被使用,可以被JVM的GC所釋放
    2.GC僅使key釋放,但是value不會釋放:
    	I.獲得key時,會刪除value
  		II.setkey時,會將附近的value刪除
    	III.手動remove刪除

結束語

目前關於JUC的面試點就總結到這裡,該篇文章後續會持續更新~

附錄

參考資料:

  1. 黑馬Java八股文面試題影片教程:併發篇-01-執行緒狀態_java中的執行緒狀態_嗶哩嗶哩_bilibili

相關文章