天天看點

Java并發程式設計高頻面試知識點,歸納總結

作者:IT三寶

八個子產品,看目錄吧,都整理好了,這些都是并發高頻面試題,也是要重點掌握的内容。

一、 線程狀态

要求

  • 01 掌握 Java 線程六種狀态
  • 02 掌握 Java 線程狀态轉換
  • 03 能了解五種狀态與六種狀态兩種說法的差別

01 六種狀态及轉換分别是

Java并發程式設計高頻面試知識點,歸納總結

建立

    • 當一個線程對象被建立,但還未調用 start 方法時處于建立狀态
    • 此時未與作業系統底層線程關聯

可運作

    • 調用了 start 方法,就會由建立進入可運作
    • 此時與底層線程關聯,由作業系統排程執行

終結

    • 線程内代碼已經執行完畢,由可運作進入終結
    • 此時會取消與底層線程關聯

阻塞

    • 當擷取鎖失敗後,由可運作進入 Monitor 的阻塞隊列阻塞,此時不占用 cpu 時間
    • 當持鎖線程釋放鎖時,會按照一定規則喚醒阻塞隊列中的阻塞線程,喚醒後的線程進入可運作狀态

等待

    • 當擷取鎖成功後,但由于條件不滿足,調用了 wait() 方法,此時從可運作狀态釋放鎖進入 Monitor 等待集合等待,同樣不占用 cpu 時間
    • 當其它持鎖線程調用 notify() 或 notifyAll() 方法,會按照一定規則喚醒等待集合中的等待線程,恢複為可運作狀态

有時限等待

    • 當擷取鎖成功後,但由于條件不滿足,調用了 wait(long) 方法,此時從可運作狀态釋放鎖進入 Monitor 等待集合進行有時限等待,同樣不占用 cpu 時間
    • 當其它持鎖線程調用 notify() 或 notifyAll() 方法,會按照一定規則喚醒等待集合中的有時限等待線程,恢複為可運作狀态,并重新去競争鎖
    • 如果等待逾時,也會從有時限等待狀态恢複為可運作狀态,并重新去競争鎖
    • 還有一種情況是調用 sleep(long) 方法也會從可運作狀态進入有時限等待狀态,但與 Monitor 無關,不需要主動喚醒,逾時時間到自然恢複為可運作狀态

其它情況(隻需了解)

  • 可以用 interrupt() 方法打斷等待、有時限等待的線程,讓它們恢複為可運作狀态
  • park,unpark 等方法也可以讓線程等待和喚醒

02 五種狀态

五種狀态的說法來自于作業系統層面的劃分

Java并發程式設計高頻面試知識點,歸納總結
  • 運作态:分到 cpu 時間,能真正執行線程内代碼的
  • 就緒态:有資格分到 cpu 時間,但還未輪到它的
  • 阻塞态:沒資格分到 cpu 時間的
    • 涵蓋了 java 狀态中提到的阻塞、等待、有時限等待
    • 多出了阻塞 I/O,指線程在調用阻塞 I/O 時,實際活由 I/O 裝置完成,此時線程無事可做,隻能幹等
  • 建立與終結态:與 java 中同名狀态類似,不再啰嗦

二、 線程池

要求

  • 掌握線程池的 7 大核心參數

并發篇-04-線程池核心參數_簡介05:53

并發篇-05-線程池核心參數_示範11:07

七大參數

  1. corePoolSize 核心線程數目 - 池中會保留的最多線程數
  2. maximumPoolSize 最大線程數目 - 核心線程+救急線程的最大數目
  3. keepAliveTime 生存時間 - 救急線程的生存時間,生存時間内沒有新任務,此線程資源會釋放
  4. unit 時間機關 - 救急線程的生存時間機關,如秒、毫秒等
  5. workQueue - 當沒有空閑核心線程時,新來任務會加入到此隊列排隊,隊列滿會建立救急線程執行任務
  6. threadFactory 線程工廠 - 可以定制線程對象的建立,例如設定線程名字、是否是守護線程等
  7. handler 拒絕政策 - 當所有線程都在繁忙,workQueue 也放滿時,會觸發拒絕政策
    • 抛異常 java.util.concurrent.ThreadPoolExecutor.AbortPolicy
    • 由調用者執行任務 java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy
    • 丢棄任務 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy
    • 丢棄最早排隊任務 java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy
Java并發程式設計高頻面試知識點,歸納總結

三、wait vs sleep

要求

并發篇-06-wait_vs_sleep_差別03:57

并發篇-07-wait_vs_sleep_示範05:43

  • 能夠說出二者差別

一個共同點,三個不同點

共同點

  • wait() ,wait(long) 和 sleep(long) 的效果都是讓目前線程暫時放棄 CPU 的使用權,進入阻塞狀态

不同點

  • 方法歸屬不同
    • sleep(long) 是 Thread 的靜态方法
    • 而 wait(),wait(long) 都是 Object 的成員方法,每個對象都有
  • 醒來時機不同
    • 執行 sleep(long) 和 wait(long) 的線程都會在等待相應毫秒後醒來
    • wait(long) 和 wait() 還可以被 notify 喚醒,wait() 如果不喚醒就一直等下去
    • 它們都可以被打斷喚醒
  • 鎖特性不同(重點)
    • wait 方法的調用必須先擷取 wait 對象的鎖,而 sleep 則無此限制
    • wait 方法執行後會釋放對象鎖,允許其它線程獲得該對象鎖(我放棄 cpu,但你們還可以用)
    • 而 sleep 如果在 synchronized 代碼塊中執行,并不會釋放對象鎖(我放棄 cpu,你們也用不了)

四、 lock vs synchronized

并發篇-08-lock_vs_synchronized_差別08:33

并發篇-09-lock_阻塞示範10:31

并發篇-10-lock_公平非公平示範07:43

并發篇-11-lock_條件變量示範06:56

  • 01 掌握 lock 與 synchronized 的差別
  • 02 了解 ReentrantLock 的公平、非公平鎖
  • 03 了解 ReentrantLock 中的條件變量

01 三個層面不同點

文法層面

    • synchronized 是關鍵字,源碼在 jvm 中,用 c++ 語言實作
    • Lock 是接口,源碼由 jdk 提供,用 java 語言實作
    • 使用 synchronized 時,退出同步代碼塊鎖會自動釋放,而使用 Lock 時,需要手動調用 unlock 方法釋放鎖

功能層面

    • 二者均屬于悲觀鎖、都具備基本的互斥、同步、鎖重入功能
    • Lock 提供了許多 synchronized 不具備的功能,例如擷取等待狀态、公平鎖、可打斷、可逾時、多條件變量
    • Lock 有适合不同場景的實作,如 ReentrantLock, ReentrantReadWriteLock

性能層面

    • 在沒有競争時,synchronized 做了很多優化,如偏向鎖、輕量級鎖,性能不賴
    • 在競争激烈時,Lock 的實作通常會提供更好的性能

02 公平鎖

  • 公平鎖的公平展現
    • 已經處在阻塞隊列中的線程(不考慮逾時)始終都是公平的,先進先出
    • 公平鎖是指未處于阻塞隊列中的線程來争搶鎖,如果隊列不為空,則老實到隊尾等待
    • 非公平鎖是指未處于阻塞隊列中的線程來争搶鎖,與隊列頭喚醒的線程去競争,誰搶到算誰的
  • 公平鎖會降低吞吐量,一般不用

03 條件變量

  • ReentrantLock 中的條件變量功能類似于普通 synchronized 的 wait,notify,用在當線程獲得鎖後,發現條件不滿足時,臨時等待的連結清單結構
  • 與 synchronized 的等待集合不同之處在于,ReentrantLock 中的條件變量可以有多個,可以實作更精細的等待、喚醒控制

五、 volatile

  • 01 掌握線程安全要考慮的三個問題
  • 02 掌握 volatile 能解決哪些問題

原子性

  • 起因:多線程下,不同線程的指令發生了交錯導緻的共享變量的讀寫混亂
  • 解決:用悲觀鎖或樂觀鎖解決,volatile 并不能解決原子性

可見性

  • 起因:由于編譯器優化、或緩存優化、或 CPU 指令重排序優化導緻的對共享變量所做的修改另外的線程看不到
  • 解決:用 volatile 修飾共享變量,能夠防止編譯器等優化發生,讓一個線程對共享變量的修改對另一個線程可見

有序性

  • 起因:由于編譯器優化、或緩存優化、或 CPU 指令重排序優化導緻指令的實際執行順序與編寫順序不一緻
  • 解決:用 volatile 修飾共享變量會在讀、寫共享變量時加入不同的屏障,阻止其他讀寫操作越過屏障,進而達到阻止重排序的效果
  • 注意:
    • volatile 變量寫加的屏障是阻止上方其它寫操作越過屏障排到 volatile 變量寫之下
    • volatile 變量讀加的屏障是阻止下方其它讀操作越過屏障排到 volatile 變量讀之上
    • volatile 讀寫加入的屏障隻能防止同一線程内的指令重排

六、悲觀鎖 vs 樂觀鎖

  • 掌握悲觀鎖和樂觀鎖的差別

對比悲觀鎖與樂觀鎖

  • 悲觀鎖的代表是 synchronized 和 Lock 鎖
    • 其核心思想是【線程隻有占有了鎖,才能去操作共享變量,每次隻有一個線程占鎖成功,擷取鎖失敗的線程,都得停下來等待】
    • 線程從運作到阻塞、再從阻塞到喚醒,涉及線程上下文切換,如果頻繁發生,影響性能
    • 實際上,線程在擷取 synchronized 和 Lock 鎖時,如果鎖已被占用,都會做幾次重試操作,減少阻塞的機會
  • 樂觀鎖的代表是 AtomicInteger,使用 cas 來保證原子性
    • 其核心思想是【無需加鎖,每次隻有一個線程能成功修改共享變量,其它失敗的線程不需要停止,不斷重試直至成功】
    • 由于線程一直運作,不需要阻塞,是以不涉及線程上下文切換
    • 它需要多核 cpu 支援,且線程數不應超過 cpu 核數

七、 Hashtable vs ConcurrentHashMap

  • 掌握 Hashtable 與 ConcurrentHashMap 的差別
  • 掌握 ConcurrentHashMap 在不同版本的實作差別

Hashtable 對比 ConcurrentHashMap

  • Hashtable 與 ConcurrentHashMap 都是線程安全的 Map 集合
  • Hashtable 并發度低,整個 Hashtable 對應一把鎖,同一時刻,隻能有一個線程操作它
  • ConcurrentHashMap 并發度高,整個 ConcurrentHashMap 對應多把鎖,隻要線程通路的是不同鎖,那麼不會沖突

ConcurrentHashMap 1.7

  • 資料結構:Segment(大數組) + HashEntry(小數組) + 連結清單,每個 Segment 對應一把鎖,如果多個線程通路不同的 Segment,則不會沖突
  • 并發度:Segment 數組大小即并發度,決定了同一時刻最多能有多少個線程并發通路。Segment 數組不能擴容,意味着并發度在 ConcurrentHashMap 建立時就固定了
  • 索引計算
    • 假設大數組長度是 2^m2m,key 在大數組内的索引是 key 的二次 hash 值的高 m 位
    • 假設小數組長度是 2^n2n,key 在小數組内的索引是 key 的二次 hash 值的低 n 位
  • 擴容:每個小數組的擴容相對獨立,小數組在超過擴容因子時會觸發擴容,每次擴容翻倍
  • Segment[0] 原型:首次建立其它小數組時,會以此原型為依據,數組長度,擴容因子都會以原型為準

ConcurrentHashMap 1.8

  1. 資料結構:Node 數組 + 連結清單或紅黑樹,數組的每個頭節點作為鎖,如果多個線程通路的頭節點不同,則不會沖突。首次生成頭節點時如果發生競争,利用 cas 而非 syncronized,進一步提升性能
  2. 并發度:Node 數組有多大,并發度就有多大,與 1.7 不同,Node 數組可以擴容
  3. 擴容條件:Node 數組滿 3/4 時就會擴容
  4. 擴容機關:以連結清單為機關從後向前遷移連結清單,遷移完成的将舊數組頭節點替換為 ForwardingNode
  5. 擴容時并發 get

    根據是否為 ForwardingNode 來決定是在新數組查找還是在舊數組查找,不會阻塞

    如果連結清單長度超過 1,則需要對節點進行複制(建立新節點),怕的是節點遷移後 next 指針改變

    如果連結清單最後幾個元素擴容後索引不變,則節點無需複制

  6. 擴容時并發 put

    如果 put 的線程與擴容線程操作的連結清單是同一個,put 線程會阻塞

    如果 put 的線程操作的連結清單還未遷移完成,即頭節點不是 ForwardingNode,則可以并發執行

    如果 put 的線程操作的連結清單已經遷移完成,即頭結點是 ForwardingNode,則可以協助擴容

  7. 與 1.7 相比是懶惰初始化
  8. capacity 代表預估的元素個數,capacity / factory 來計算出初始數組大小,需要貼近 2^n2n
  9. loadFactor 隻在計算初始數組大小時被使用,之後擴容固定為 3/4
  10. 超過樹化門檻值時的擴容問題,如果容量已經是 64,直接樹化,否則在原來容量基礎上做 3 輪擴容

八、ThreadLocal

  • 01 掌握 ThreadLocal 的作用與原理
  • 02 掌握 ThreadLocal 的記憶體釋放時機

01ThreadLocal 的作用與原理

作用

  • ThreadLocal 可以實作【資源對象】的線程隔離,讓每個線程各用各的【資源對象】,避免争用引發的線程安全問題
  • ThreadLocal 同時實作了線程内的資源共享

原理

每個線程内有一個 ThreadLocalMap 類型的成員變量,用來存儲資源對象

  • 調用 set 方法,就是以 ThreadLocal 自己作為 key,資源對象作為 value,放入目前線程的 ThreadLocalMap 集合中
  • 調用 get 方法,就是以 ThreadLocal 自己作為 key,到目前線程中查找關聯的資源值
  • 調用 remove 方法,就是以 ThreadLocal 自己作為 key,移除目前線程關聯的資源值

ThreadLocalMap 的一些特點

  • key 的 hash 值統一配置設定
  • 初始容量 16,擴容因子 2/3,擴容容量翻倍
  • key 索引沖突後用開放尋址法解決沖突

弱引用 key

ThreadLocalMap 中的 key 被設計為弱引用,原因如下

  • Thread 可能需要長時間運作(如線程池中的線程),如果 key 不再使用,需要在記憶體不足(GC)時釋放其占用的記憶體

02 記憶體釋放時機

被動 GC 釋放 key

    • 僅是讓 key 的記憶體釋放,關聯 value 的記憶體并不會釋放

懶惰被動釋放 value

    • get key 時,發現是 null key,則釋放其 value 記憶體
    • set key 時,會使用啟發式掃描,清除臨近的 null key 的 value 記憶體,啟發次數與元素個數,是否發現 null key 有關

主動 remove 釋放 key,value

    • 會同時釋放 key,value 的記憶體,也會清除臨近的 null key 的 value 記憶體
    • 推薦使用它,因為一般使用 ThreadLocal 時都把它作為靜态變量(即強引用),是以無法被動依靠 GC 回收

java面試專題課java面試寶典(含阿裡、騰迅大廠java面試真題,java資料結構,java并發,jvm等最新java面試真題)以100+企業大廠真實高頻Java面試真題為主幹,輔以資料結構的可視化展示、算法的可視化展示,窺探底層的工具使用等等可視化手段,用最直覺、形象的方式展現複雜的知識内容。

繼續閱讀