多執行緒常見面試題一
1、Java執行緒安全的類
1、String 因其被final 修飾,所以具有不可變性,天生就執行緒安全2、VectorStack
,HashTable
,StringBuffer
通過synchronized
關鍵字給方法加上內建鎖來實現執行緒安全3、原子類Atomicxxx
—包裝類的執行緒安全類
如AtomicLong
,AtomicInteger
等等
Atomicxxx
是通過Unsafe
類的native方法實現執行緒安全的
通過 {
unsafe CAS操作
Volitle 修飾屬性,保證可見
}
4、BlockingQueue 和BlockingDeque
BlockingDeque介面繼承了BlockingQueue介面,
BlockingQueue 介面的實現類有ArrayBlockingQueue ,LinkedBlockingQueue ,PriorityBlockingQueue 而BlockingDeque介面的實現類有LinkedBlockingDeque
BlockingQueue和BlockingDeque 都是通過使用定義為final的ReentrantLock作為類屬性顯式加鎖實現同步的
5、CopyOnWriteArrayList和 CopyOnWriteArraySet(讀寫分離)CopyOnWriteArraySet的內部實現是在其類內部宣告一個final的CopyOnWriteArrayList屬性,並在呼叫其建構函式時例項化該CopyOnWriteArrayList,2、volatile關鍵字作用
volatile 關鍵字的作用
常用於保持變數對所有執行緒的可見性(隨時見到的都是最新值)和防止指令重排序。可見性是指:一個執行緒對變數的修改對其他執行緒是可見的。volatile是通過定義特殊規則來實現可見性的:
- read、load、use動作必須連續出現。
- assign、store、write動作必須連續出現。
所以,使用volatile變數能夠保證:
- 每次
讀取前
必須先從主記憶體重新整理最新的值。 - 每次
寫入後
必須立即同步回主記憶體當中。
volatile 是通過 記憶體屏障來實現指令重排序的。
在偏序關係
的Happens-Before記憶體模型
中,指令重排技術大大提高了程式執行效率。但同時也帶來了一些問題。比如一個比較經典的問題就是基於 DCL 雙鎖檢查的單例設計模式,如果沒有把成員 instance 宣告為 valotile, 那麼在建立物件的時候將會對 建立物件操作這個底層實現進行排序優化,建立物件的抽象過程我們認為應該是先分配記憶體,然後初始化物件,最後返回物件的引用,但是實際上 cpu 會將這個過程進行 重排序,實際的建立過程是 先分配記憶體,然後返回物件引用,最後初始化物件。所以這個重排序就導致了,如果一個執行緒剛好處於建立單例物件的第二步和第三步之間,如果另一個執行緒呼叫getInstance方法,由於instance已經指向了一塊記憶體空間,從而if條件判為false,方法返回instance引用,使用者得到了沒有完成初始化的“半個”單例。所以如果要實現安全的 單例,就可以使用 對instance 成員生宣告為 volatile 來實現。
JMM記憶體屏障插入策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障。
- 在每個volatile寫操作的後面插入一個StoreLoad屏障。
- 在每個volatile讀操作的前面插入一個LoadLoad屏障。
- 在每個volatile讀操作的後面插入一個LoadStore屏障
注意:
volatile 是沒有原子性的。但對volatile的使用過程中很容易出現的一個問題是:
錯把volatile變數當做原子變數。
出現這種誤解的原因,主要是volatile關鍵字使變數的讀、寫具有了“原子性”。然而這種原子性僅限於變數(包括引用)的讀和寫,無法涵蓋變數上的任何操作,即:
- 基本型別的自增(如
count++
)等操作不是原子的。 - 物件的任何非原子成員呼叫(包括
成員變數
和成員方法
)不是原子的。
如果希望上述操作也具有原子性,那麼只能採取鎖、原子類更多的措施。
參考:
面試題:volatile關鍵字的作用、原理(好文)一文解決記憶體屏障3、有哪些鎖?可重入不可重入?自旋鎖互斥鎖可重入?
3.1、獨享鎖/共享鎖
獨享鎖是指該鎖一次只能被一個執行緒所持有。 (ReentrantLock、 Synchronized)
共享鎖是指該鎖可被多個執行緒所持有。 (ReadWriteLock 讀鎖是共享鎖,寫鎖是獨享鎖。 )
3.2、 公平鎖/非公平鎖
公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖。
非公平鎖是指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先獲取鎖。有可能會造成飢餓現象。
Synchronized 非公平鎖。ReentrantLock預設是非公平鎖,不過可以通過建構函式傳入 true 這個 boolean 值來指定該鎖是公平鎖,。非公平鎖的優點在於吞吐量比公平鎖大。
3.3、可重入鎖
可重入鎖又名遞迴鎖,是指同一個執行緒在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。
ReentrantLock和Synchronized都是可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖,比如 A B 方法都鎖定的是同一個物件,然後A 方法中呼叫了 B 方法,如果外層方法獲取鎖之後內層方法還需要獲取鎖,那麼這個執行緒就會等待持有鎖的執行緒釋放鎖,但是持有鎖的執行緒是它本身,所以它在等待自己釋放一個自己持有的鎖,就陷入了死鎖。
需要注意的是,可重入鎖加鎖和解鎖的次數要相等。不過一般加鎖和解鎖都是成對出現的,所以這個一般不會出現問題。
3.4、樂觀鎖/悲觀鎖
樂觀鎖/悲觀鎖不是指具體型別的鎖,而是看待併發的角度。悲觀鎖認為存在很多併發更新操作,採取加鎖操作,如果不加鎖一定會有問題樂觀鎖認為不存在很多的併發更新操作,不需要加鎖。資料庫中樂觀鎖的實現一般採用版本號,Java中可使用CAS實現樂觀鎖。
3.5、分段鎖
分段鎖是一種鎖的設計,並不是一種具體的鎖。對於 JDK 1.7 以 1.7 以前的 ConcuttentHashMap 就是通過分段鎖實現高效的併發操作。
3.6、自旋鎖和阻塞鎖
自旋鎖是指嘗試獲取鎖的執行緒不會阻塞,而是採用一段空迴圈的方式等待持有鎖的執行緒釋放鎖,然後獲取鎖。好處是減少上下文切換,缺點是一直佔用CPU資源。
阻塞鎖就是當獲取不到的時候就進入阻塞狀態,等待作業系統喚醒,需要上下文切換,開銷大。3.7、 偏向鎖/輕量級鎖/重量級鎖
這是jdk1.6中對Synchronized鎖做的優化
從jdk1.6開始為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”。鎖共有四種狀態,級別從低到高分別是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。隨著競爭情況鎖狀態逐漸升級、鎖可以升級但不能降級。
4、針對 Synchronized 關鍵字的鎖優化進位制
鎖的優化機制主要有 :鎖粗化,鎖消除,JDK 1.6 還引入了 偏向鎖,自旋鎖,還有輕量級鎖。4.1 鎖粗化:
如果虛擬機器探測到有這樣一串零碎的操作都對同一個物件加鎖,將會把加鎖同步的範圍擴充套件到整個操作序列的外部,這樣就只需要加鎖一次就夠了
4.2鎖消除:
如果你定義的類的方法上有同步鎖,但在執行時,卻只有一個執行緒在訪問,此時逃逸分析後的機器碼,會去掉同步鎖執行。
4.3 自旋鎖:
自旋鎖是指嘗試獲取鎖的執行緒不會阻塞,而是採用迴圈的方式嘗試獲取鎖。好處是減少上下文切換,缺點是一直佔用CPU資源。4.4 偏向鎖 / 輕量級鎖
1.偏向鎖是為了避免某個執行緒反覆獲得/釋放同一把鎖時的效能消耗,如果仍然是同個執行緒去獲得這個鎖,嘗試偏向鎖時會直接進入同步塊,不需要再次獲得鎖。2.而輕量級鎖和自旋鎖都是為了避免直接呼叫作業系統層面的互斥操作,因為掛起執行緒是一個很耗資源的操作。為了儘量避免使用重量級鎖(作業系統層面的互斥),首先會嘗試輕量級鎖,輕量級鎖會嘗試使用CAS操作來獲得鎖,如果輕量級鎖獲得失敗,說明存在競爭。但是也許很快就能獲得鎖,就會嘗試自旋鎖,將執行緒做幾個空迴圈,每次迴圈時都不斷嘗試獲得鎖。如果自旋鎖也失敗,那麼只能升級成重量級鎖。3.可見偏向鎖,輕量級鎖,自旋鎖都是樂觀鎖。偏向鎖的獲取和撤銷:
HotSpot作者經過研究發現,大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入偏向鎖。
執行緒1檢查物件頭中的Mark Word中是否儲存了執行緒1,如果沒有則CAS操作將Mark Word中的執行緒ID替換為執行緒1。此時,鎖偏向執行緒1,後面該執行緒進入同步塊時不需要進行CAS操作,只需要簡單的測試一下Mark Word中是否儲存指向當前執行緒的偏向鎖,如果成功表明該執行緒已經獲得鎖。如果失敗,則再需要測試一下Mark Word中偏向鎖標識是否設定為1(是否是偏向鎖),如果沒有設定,則使用CAS競爭鎖,如果設定了,則嘗試使用CAS將偏向鎖指向當前執行緒
偏向鎖的競爭結果:
根據持有偏向鎖的執行緒是否存活
1.如果不活動,偏向鎖撤銷到無鎖狀態,再偏向到其他執行緒
2.如果執行緒仍然活著,則升級到輕量級鎖
輕量級鎖膨脹:
1.執行緒在執行同步塊之前,JVM會在當前棧楨中建立用於儲存鎖記錄的空間(Lock record),並將物件頭中的Mark Word複製到鎖記錄中(Displaced Mark Word)。
2.然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標
3.如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒嘗試使用自旋來獲取鎖。在自旋次數超過一定次數,則將 物件頭 升級為 重量級鎖,當前執行緒不再自旋,陷入阻塞。
輕量級鎖的釋放
用 CAS 操作 把 Lock Record 中的副本拷貝到 物件頭的 MarkWord 中,如果替換成功,則整個同步過程就順利完成了;如果替換失敗,說明現在的鎖已經是重量級鎖了,說明有其他執行緒嘗試獲取過該鎖,就要在釋放鎖的同時,喚醒被掛起的執行緒。
參考:Java中常用的鎖機制(好文強推)5、什麼是逃逸分析
逃逸分析的基本行為就是分析物件的動態作用域。當一個物件在方法中被定義後,它可能被外部方法所引用(例如作為形參傳遞到其它方法中去),稱為方法逃逸。如果是被外部執行緒訪問到,稱為執行緒逃逸。如果能夠證明一個物件不會逃逸到方法或者執行緒之外,則可能對這個物件進行一些高效的優化:
- 棧上分配
如果能夠確定一個物件不會逃逸到方法之外,可以在棧上分配物件的記憶體,這樣物件佔用的記憶體空間可以隨著棧幀出棧而銷燬,減少gc的壓力; - 同步消除
如果逃逸分析得出物件不會逃逸到執行緒之外,那麼物件的同步措施可以消除。 - 標量替換
如果逃逸分析證明一個物件不會被外部訪問,並且這個物件可以被拆解,那麼程式執行的時候可能不建立這個物件,改為在棧上分配這個方法所用到的物件的成員變數。
常見的發生逃逸的場景有:
給全域性變數賦值,方法返回值,例項引用作為引數傳遞參考:即時編譯(JIT)
6、AQS
AQS介紹
AbstractQueuedSynchronizer:抽象同步佇列,簡稱AQS。AQS是JDK下提供的一套用於實現基於FIFO等待佇列的阻塞鎖和相關的同步器的一個同步框架。主要依賴一個 int 成員變數state來表示同步狀態,以及一個管理等待鎖的執行緒的CLH 等待佇列AQS的等待佇列是一個CLH(Craig, Landin, and Hagersten lock queue)佇列:競爭資源同一時間只能被一個執行緒訪問, CLH為管理等待鎖的執行緒的佇列
synchronized 能夠對一個需要確保執行緒安全的物件、方法實現多執行緒併發控制,這是在java語法層次的實現,而AbstractQueuedSynchronizer 則是在應用層次而不是語法層次(更高的層次)提供了實現多執行緒併發控制組件的基礎。可見 CountDownLatch 是基於AQS框架來實現的一個同步器.類似的同步器在JUC下還有不少。(eg. Semaphore )
一. AQS 是構建同步器的【框架】
【核心思想】 : 執行緒請求資源
情況1 : 資源空閒
則 請求執行緒設定為工作執行緒,資源上鎖
情況2 : 資源被佔用
則 請求執行緒阻塞,加入CLH佇列。等待資源空閒時競爭資源
二. AQS 定義兩種 資源共享模式
1. 獨佔鎖 Exclusive : 鎖只能被一個執行緒佔有
例如 : ReentrantLock 又分為 公平鎖和非公平鎖
2. 共享鎖 shared : 多個執行緒共享鎖
例如 : CountDownLatch 、Semaphore
三. AQS框架 自定義模組
嘗試 獲取/釋放 獨佔資源
tryAcquire()
tryRelease()
嘗試 獲取/釋放共享資源
tryAcquireShared()
tryReleaseShared()
四. AQS 常見元件
1. ReentrantLock
A 執行緒呼叫 lock()方法
若 state=0 ,
則資源空閒 ,state++,且 A執行緒可重複獲取鎖
若 state!=0 ,
則資源被佔有,當state=0時其他執行緒才能競爭
2. CountDownLatch
(1) 構造器初始化 【state = N】
當【子執行緒】呼叫countDown(),通過 CAS操作state自減1
當state=0 時,呼叫await的執行緒 恢復正常繼續執行
只有達到一定數量的執行緒,才能突破關卡,繼續執行
3. CyclicBarrier
構造方法 state=n
每當一個執行緒呼叫 await()方法,則CAS操作state自減1
當state=0 時 ,所有呼叫await()的執行緒恢復
好比是所有執行緒約定一起出去玩,直到所有執行緒都到了才可以出發
AQS原始碼
1. aquire()
public void aquire(){
if(!tryAcquire() // 嘗試獲取一次
&& acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
// acquireQueued 【作用】: 自旋檢測 (tryAcquire()&& node==head.next)
// addWaiter【作用】: 添加當前執行緒node至 佇列尾部
selfInterrupt();
}
【問題】: 為何不僅呼叫 acuqireQueued(addWaiter())
優先嚐試最可能成功的程式碼, 可減少執行的位元組碼指令
jdk中哪種資料結構或工具可以實現當多個執行緒到達某個狀態時執行一段程式碼,柵欄和閉鎖的區別
CountDownLatch 和 CyclicBarrierCountDownLatch
又稱為閉鎖,是一個同步輔助類,允許一個或者多個執行緒等待某個事件的發生,事件沒有發生前,所有執行緒將阻塞等待;而事件發生後,所有執行緒將開始執行;維護了一個計數器 cnt,每次呼叫countDown()方法會讓計數器的值減 1,減到 0 的時候,那些因為呼叫 await() 方法而在等待的執行緒就會被喚醒,繼續執行後面的程式碼。應用場景:
確保某個計算在其需要的所有資源都被初始化之後才繼續執行。二元閉鎖(包括兩個狀態)可以用來表示“資源R已經被初始化”,而所有需要R的操作都必須先在這個閉鎖上等待。 確保某個服務在其依賴的所有其他服務都已經啟動之後才啟動。 等待直到某個操作的所有參與者都就緒才繼續執行。(例如:多人遊戲中需要所有玩家準備才能開始)CyclicBarrier
用來控制多個執行緒互相等待,只有當所有執行緒都到達時,這些執行緒才會繼續執行。和 CountdownLatch 相似,都是通過維護計數器來實現的。執行緒執行 await() 方法之後計數器會減 1,並進行等待,直到計數器為 0,所有呼叫 await() 方法而在等待的執行緒才能繼續執行。區別:
閉鎖用於所有執行緒等待一個外部事件的發生,比如只有達到一定數量的執行緒,才能突破關卡,繼續執行;柵欄則是所有執行緒相互等待,好比是所有執行緒約定一起出去玩,直到所有執行緒都到了才可以出發。直到所有執行緒都到達某一點時才打開柵欄(可以理解為一個內部事件),然後執行緒可以繼續執行。它們的另一個區別是,CyclicBarrier 的計數器通過呼叫 reset() 方法可以迴圈使用,所以它才叫做迴圈屏障。參考:閉鎖CountDownLatch與柵欄CyclicBarrierjava多執行緒併發系列之閉鎖(Latch)和柵欄(CyclicBarrier)
如何使用訊號量實現上述情況
7、ThreadLocal的原理(下面只是簡單概括,詳細原理檢視《ThreadLocal原理,記憶體洩漏問題,怎麼解決》)
每個Thread類中有一個ThreadLocalMap物件,這個ThreadLocalMap底層是一個鍵值對陣列,每個鍵值對的鍵是ThreadLocal引用,值是我們要儲存的資料物件。當我用呼叫ThreadLcoal物件的set()方法時, ThreadLocal物件會獲取到當前當前執行緒的引用,根據這個引用獲取到執行緒的成員ThreadLocalMap物件,然後後呼叫ThreadLocalMap物件的set方法儲存到這個Map中。看似我們是把資料儲存在了ThreadLcoal物件中,但是實際上我們是把資料儲存在當前執行緒的ThreadLocalMap中。ThreadLocal的get()方法也是類似,先獲取當前執行緒物件引用,然後獲取這個執行緒的成員物件ThreadLocalMap,以 ThreadLocal 引用為鍵,取出這個鍵值對中的值。因為每個健在ThreadMap中是唯一的,它唯一標識了一個健值對,所以我們在ThreadLocalMap中不能儲存多個健相等的鍵值對,而因為這個ThreadLocalMap是以ThreadLocal物件引用為健值,所以一個ThreadLocalMap物件只能儲存一個以同一個ThreadLocal物件引用為鍵值的鍵值對,也就是每個執行緒對同一個ThreadLocal物件,只能儲存一個數據物件。
8、為什麼有了lock之後synchronized沒被廢棄掉,反而進行了鎖的優化
在解決死鎖問題的時候,提出了一個破壞不可搶佔條件方案,但是這個方案 synchronized 沒有辦法解決。原因是 synchronized 申請資源的時候,如果申請不到,執行緒直接進入阻塞狀態了,而執行緒進入阻塞狀態,啥都幹不了,也釋放不了執行緒已經佔有的資源。但我們希望的是:
如果我們重新設計一把互斥鎖去解決這個問題,那該怎麼設計呢?我覺得有三種方案。對於“不可搶佔”這個條件,佔用部分資源的執行緒進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源,這樣不可搶佔這個條件就破壞掉了。
- 能夠響應中斷。synchronized 的問題是,持有鎖 A 後,如果嘗試獲取鎖 B 失敗,那麼執行緒就進入阻塞狀態,一旦發生死鎖,就沒有任何機會來喚醒阻塞的執行緒。但如果阻塞狀態的執行緒能夠響應中斷訊號,也就是說當我們給阻塞的執行緒傳送中斷訊號的時候,能夠喚醒它,那它就有機會釋放曾經持有的鎖 A。這樣就破壞了不可搶佔條件了。
- 支援超時。如果執行緒在一段時間之內沒有獲取到鎖,不是進入阻塞狀態,而是返回一個錯誤,那這個執行緒也有機會釋放曾經持有的鎖。這樣也能破壞不可搶佔條件。
- 非阻塞地獲取鎖。如果嘗試獲取鎖失敗,並不進入阻塞狀態,而是直接返回,那這個執行緒也有機會釋放曾經持有的鎖。這樣也能破壞不可搶佔條件。
這三種方案可以全面彌補 synchronized 的問題。到這裡相信你應該也能理解了,這三個方案就是“重複造輪子”的主要原因,體現在 API 上,就是 Lock 介面的三個方法。詳情如下:
// 支援中斷的 API
void lockInterruptibly() throws InterruptedException;
// 支援超時的 API
boolean tryLock(long time, TimeUnit unit)throws InterruptedException;
// 支援非阻塞獲取鎖的 API
boolean tryLock();
synchronized 底層,先講下面的,然後將鎖的優化Sychronized 修飾 程式碼塊 || 方法
1.修飾程式碼塊時
通過 【monitorenter 和 monitorExit 兩條指令】,分別指定同步程式碼塊的 開始位置和結束位置。
執行緒獲取鎖 = 獲取位於物件頭的monitor的持有權
獲取到鎖,則計數器++。 執行到monitorExit,則計數器--
2.修飾方法
JVM通過 ACC_SYNCHRONIZED 辨別方法為同步方法
【面試口頭】
Sychronized 是【JVM】層面的關鍵字。它是通過 【位元組碼指令】實現的。
(1) Sychronized 修飾 【程式碼塊】時,montior-enter monitor-exit兩個位元組碼指令表明
同步塊的開始和結束位置。
(2) Sychronized 修飾 【方法】時,JVM中通過ACC_SYCHRONIZED 標誌同步方法
9、Lock和Condition
在併發程式設計領域,有兩大核心問題:一個是互斥,即同一時刻只允許一個執行緒訪問共享資源;另一個是同步,即執行緒之間如何通訊、協作。這兩大問題,管程都是能夠解決的。Java SDK 併發包通過 Lock 和 Condition 兩個介面來實現管程,其中 Lock 用於解決互斥問題,Condition 用於解決同步問題。支援超時、非阻塞、可中斷的方式獲取鎖,這三種方式為我們編寫更加安全、健壯的併發程式提供了很大的便利。ReentrantLock 和 Sychronized 區別
1. 兩者都是【可重入鎖】 :
外層方法獲得鎖之後,內層方法如果獲取的是同一把鎖,則可以直接獲取鎖,無需阻塞,這樣一定程度上可以
避免死鎖問題
2. Sychronized 依賴JVM實現,而ReentrantLock 依賴API實現(JDK層面)
ReentrantLock 呼叫 lock() unlock() try/finally語句 實現同步塊,可以直接檢視原始碼
Sychronized 在JVM層面,通過位元組碼指令 monitorEnter monitorExit指定同步塊的開始和結束位置
3. ReentrantLock 實現高階功能
(1) ReentrantLock實現等待可中斷 :
通過呼叫 lockInterruptibly() 中斷等待鎖的執行緒
(2) ReentrantLock可實現公平鎖,而Sychronized僅實現非公平鎖:
公平鎖 = 先等待的執行緒,先獲得鎖
(3) 等待/通知機制 不同:
Sychronized 通過 notiy() notifyAll() wait() 實現等待/通知機制
ReentrantLock 通過 Condition物件實現。
一個lock可建立多個Condition物件,一個Condition物件可註冊多個執行緒。
Condition 物件呼叫signal ||signalAll()
喚醒執行緒所在範圍 = 註冊的執行緒,
而Sychronized 呼叫 notify() || notifyAll()
喚醒執行緒 = JVM選擇的
因此 ReentrantLock的等待通知機制更加靈活
10、Thread的start方法和run方法的區別?
run方法就是普通的一個方法,程式碼執行在當前主執行緒,start會啟動一個新的執行緒,並執行run方法。
參考:
Java常見的執行緒安全的類面試題:volatile關鍵字的作用、原理(好文)一文解決記憶體屏障即時編譯(JIT)Java中常用的鎖機制(好文強推)
閉鎖CountDownLatch與柵欄CyclicBarrierjava多執行緒併發系列之閉鎖(Latch)和柵欄(CyclicBarrier)Cyc2018