大家好,我是大彬~
今天給大家分享Java并發高頻面試題(第二彈),助力春招上岸!
文章目錄如下:
- volatile底層原理
- synchronized的用法有哪些?
- synchronized的作用有哪些?
- synchronized 底層實作原理?
- volatile和synchronized的差別?
- ReentrantLock和synchronized差別?
- wait()和sleep()的異同點?
- Runnable和Callable有什麼差別?
- 守護線程是什麼?
- 線程間通信方式
- AQS原理
- ReentrantLock 是如何實作可重入性的?
- 鎖的分類
- 公平鎖與非公平鎖
- 共享式與獨占式鎖
- 悲觀鎖與樂觀鎖
- 樂觀鎖有什麼問題?
- 什麼是CAS?
volatile底層原理
volatile
是輕量級的同步機制,
volatile
保證變量對所有線程的可見性,不保證原子性。
- 當對
變量進行寫操作的時候,JVM會向處理器發送一條volatile
字首的指令,将該變量所在緩存行的資料寫回系統記憶體。LOCK
- 由于緩存一緻性協定,每個處理器通過嗅探在總線上傳播的資料來檢查自己的緩存是不是過期了,當處理器發現自己緩存行對應的記憶體位址被修改,就會将目前處理器的緩存行置為無效狀态,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器緩存中。
來看看緩存一緻性協定是什麼。
緩存一緻性協定:當CPU寫資料時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信号通知其他CPU将該變量的緩存行置為無效狀态,是以當其他CPU需要讀取這個變量時,就會從記憶體重新讀取。
volatile
關鍵字的兩個作用:
- 保證了不同線程對共享變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
- 禁止進行指令重排序。
指令重排序是JVM為了優化指令,提高程式運作效率,在不影響單線程程式執行結果的前提下,盡可能地提高并行度。Java編譯器會在生成指令系列時在适當的位置會插入 記憶體屏障
指令來禁止處理器重排序。插入一個記憶體屏障,相當于告訴CPU和編譯器先于這個指令的必須先執行,後于這個指令的必須後執行。對一個volatile字段進行寫操作,Java記憶體模型将在寫操作後插入一個寫屏障指令,這個指令會把之前的寫入值都重新整理到記憶體。
synchronized的用法有哪些?
- 修飾普通方法:作用于目前對象執行個體,進入同步代碼前要獲得目前對象執行個體的鎖
- 修飾靜态方法:作用于目前類,進入同步代碼前要獲得目前類對象的鎖,synchronized關鍵字加到static 靜态方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖
- 修飾代碼塊:指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖
synchronized的作用有哪些?
原子性:確定線程互斥的通路同步代碼;
可見性:保證共享變量的修改能夠及時可見;
有序性:有效解決重排序問題。
synchronized 底層實作原理?
synchronized 同步代碼塊的實作是通過
monitorenter
和
monitorexit
指令,其中
monitorenter
指令指向同步代碼塊的開始位置,
monitorexit
指令則指明同步代碼塊的結束位置。當執行
monitorenter
指令時,線程試圖擷取鎖也就是擷取
monitor
的持有權。
monitor對象存在于每個Java對象的對象頭中, synchronized 鎖便是通過這種方式擷取鎖的,也是為什麼Java中任意對象可以作為鎖的原因
其内部包含一個計數器,當計數器為0則可以成功擷取,擷取後将鎖計數器設為1也就是加1。相應的在執行
monitorexit
指令後,将鎖計數器設為0 ,表明鎖被釋放。如果擷取對象鎖失敗,那目前線程就要阻塞等待,直到鎖被另外一個線程釋放為止
synchronized 修飾的方法并沒有
monitorenter
指令和
monitorexit
指令,取得代之的确實是
ACC_SYNCHRONIZED
辨別,該辨別指明了該方法是一個同步方法,JVM 通過該
ACC_SYNCHRONIZED
通路标志來辨識一個方法是否聲明為同步方法,進而執行相應的同步調用。
volatile和synchronized的差別?
-
隻能使用在變量上;而volatile
可以在類,變量,方法和代碼塊上。synchronized
-
至保證可見性;volatile
保證原子性與可見性。synchronized
-
禁用指令重排序;volatile
不會。synchronized
-
不會造成阻塞;volatile
會。synchronized
ReentrantLock和synchronized差別?
- 使用synchronized關鍵字實作同步,線程執行完同步代碼塊會自動釋放鎖,而ReentrantLock需要手動釋放鎖。
- synchronized是非公平鎖,ReentrantLock可以設定為公平鎖。
- ReentrantLock上等待擷取鎖的線程是可中斷的,線程可以放棄等待鎖。而synchonized會無限期等待下去。
- ReentrantLock 可以設定逾時擷取鎖。在指定的截止時間之前擷取鎖,如果截止時間到了還沒有擷取到鎖,則傳回。
- ReentrantLock 的 tryLock() 方法可以嘗試非阻塞的擷取鎖,調用該方法後立刻傳回,如果能夠擷取則傳回true,否則傳回false。
wait()和sleep()的異同點?
相同點:
- 使目前線程暫停運作,把機會交給其他線程
- 任何線程在等待期間被中斷都會抛出
InterruptedException
不同點:
-
是Object超類中的方法;而wait()
是線程Thread類中的方法sleep()
- 對鎖的持有不同,
會釋放鎖,而wait()
并不釋放鎖sleep()
- 喚醒方法不完全相同,
依靠wait()
或者notify
、中斷、達到指定時間來喚醒;而notifyAll
到達指定時間被喚醒sleep()
- 調用
需要先擷取對象的鎖,而wait()
不用Thread.sleep()
Runnable和Callable有什麼差別?
- Callable接口方法是
,Runnable的方法是call()
;run()
- Callable接口call方法有傳回值,支援泛型,Runnable接口run方法無傳回值。
- Callable接口
方法允許抛出異常;而Runnable接口call()
方法不能繼續上抛異常。run()
守護線程是什麼?
守護線程是運作在背景的一種特殊程序。它獨立于控制終端并且周期性地執行某種任務或等待處理某些發生的事件。在 Java 中垃圾回收線程就是特殊的守護線程。
線程間通信方式
volatile
volatile 使用共享記憶體實作線程間互相通信。多個線程同時監聽一個變量,當這個變量被某一個線程修改的時候,其他線程可以感覺到這個變化。
wait和 notify
wait/notify
為Object對象的方法,調用
wait/notify
需要先獲得對象的鎖。對象調用
wait()
之後線程釋放鎖,将線程放到對象的等待隊列,當通知線程調用此對象的
notify()
方法後,等待線程并不會立即從
wait()
傳回,需要等待通知線程釋放鎖(通知線程執行完同步代碼塊),等待隊列裡的線程擷取鎖,擷取鎖成功才能從
wait()
方法傳回,即從
wait()
方法傳回前提是線程獲得鎖。
join
當在一個線程調用另一個線程的
join()
方法時,目前線程阻塞等待被調用join方法的線程執行完畢才能繼續執行。
join()
是基于等待通知機制實作的。
AQS原理
AQS,
AbstractQueuedSynchronizer
,抽象隊列同步器,定義了一套多線程通路共享資源的同步器架構,許多并發工具的實作都依賴于它,如常用的
ReentrantLock/Semaphore/CountDownLatch
。
AQS使用一個
volatile
的int類型的成員變量
state
來表示同步狀态,通過CAS修改同步狀态的值。當線程調用 lock 方法時 ,如果
state=0
,說明沒有任何線程占有共享資源的鎖,可以獲得鎖并将
state
加1。如果
state
不為0,則說明有線程目前正在使用共享變量,其他線程必須加入同步隊列進行等待。
private volatile int state;//共享變量,使用volatile修飾保證線程可見性
複制
同步器依賴内部的同步隊列(一個FIFO雙向隊列)來完成同步狀态的管理,目前線程擷取同步狀态失敗時,同步器會将目前線程以及等待狀态(獨占或共享)構造成為一個節點(Node)并将其加入同步隊列并進行自旋,當同步狀态釋放時,會把首節點中的後繼節點對應的線程喚醒,使其再次嘗試擷取同步狀态。
ReentrantLock 是如何實作可重入性的?
ReentrantLock
内部自定義了同步器sync,在加鎖的時候通過CAS算法,将線程對象放到一個雙向連結清單中,每次擷取鎖的時候,會檢查目前占有鎖的線程和目前請求鎖的線程是否一緻,如果一緻,同步狀态加1,表示鎖被目前線程擷取了多次。
源碼如下:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
複制
鎖的分類
公平鎖與非公平鎖
按照線程通路順序擷取對象鎖。
synchronized
是非公平鎖,
Lock
預設是非公平鎖,可以設定為公平鎖,公平鎖會影響性能。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
複制
共享式與獨占式鎖
共享式與獨占式的最主要差別在于:同一時刻獨占式隻能有一個線程擷取同步狀态,而共享式在同一時刻可以有多個線程擷取同步狀态。例如讀操作可以有多個線程同時進行,而寫操作同一時刻隻能有一個線程進行寫操作,其他操作都會被阻塞。
悲觀鎖與樂觀鎖
悲觀鎖,每次通路資源都會加鎖,執行完同步代碼釋放鎖,
synchronized
和
ReentrantLock
屬于悲觀鎖。
樂觀鎖,不會鎖定資源,所有的線程都能通路并修改同一個資源,如果沒有沖突就修改成功并退出,否則就會繼續循環嘗試。樂觀鎖最常見的實作就是
CAS
。
适用場景:
- 悲觀鎖适合寫操作多的場景。
- 樂觀鎖适合讀操作多的場景,不加鎖可以提升讀操作的性能。
樂觀鎖有什麼問題?
樂觀鎖避免了悲觀鎖獨占對象的問題,提高了并發性能,但它也有缺點:
- 樂觀鎖隻能保證一個共享變量的原子操作。
- 長時間自旋可能導緻開銷大。假如CAS長時間不成功而一直自旋,會給CPU帶來很大的開銷。
- ABA問題。CAS的原理是通過比對記憶體值與預期值是否一樣而判斷記憶體值是否被改過,但是會有以下問題:假如記憶體值原來是A, 後來被一條線程改為B,最後又被改成了A,則CAS認為此記憶體值并沒有發生改變。可以引入版本号解決這個問題,每次變量更新都把版本号加一。
什麼是CAS?
CAS全稱
Compare And Swap
,比較與交換,是樂觀鎖的主要實作方式。CAS在不使用鎖的情況下實作多線程之間的變量同步。
ReentrantLock
内部的AQS和原子類内部都使用了CAS。
CAS算法涉及到三個操作數:
- 需要讀寫的記憶體值V。
- 進行比較的值A。
- 要寫入的新值B。
隻有當V的值等于A時,才會使用原子方式用新值B來更新V的值,否則會繼續重試直到成功更新值。
以
AtomicInteger
為例,
AtomicInteger
的
getAndIncrement()
方法底層就是CAS實作,關鍵代碼是
compareAndSwapInt(obj, offset, expect, update)
,其含義就是,如果
obj
内的
value
和
expect
相等,就證明沒有其他線程改變過這個變量,那麼就更新它為
update
,如果不相等,那就會繼續重試直到成功更新值。
大彬,非科班出身,自學Java,校招斬獲京東、攜程、華為等offer。作為一名轉碼選手,深感這一路的不易。
希望我的分享能幫助到更多的小夥伴,我踩過的坑你們不要再踩。