Java面試題沖刺第二十四天–並發編程

面試題1:說一下你對ReentrantLock的理解?

ReentrantLock是JDK1.5引入的,它擁有與synchronized相同的並發性和內存語義,並提供瞭超出synchonized的其他高級功能(例如,中斷鎖等候、條件變量等),並且使用ReentrantLock比synchronized能獲得更好的可伸縮性。

ReentrantLock主要利用: CAS(compare-and-swap) + AQS(AbstractQueuedSynchronizer)隊列來實現。它支持公平鎖和非公平鎖,兩者的實現類似。

CAS:

CAS(compare-and-swap),見名知意,比較並交換。CAS 加 volatile 關鍵字是實現並發包的基石。沒有CAS就不會有並發包,synchronized是一種獨占鎖、悲觀鎖,java.util.concurrent中借助瞭CAS指令實現瞭一種區別於synchronized的一種樂觀鎖。

CAS引用瞭樂觀鎖思想,每次拿數據的時候都認為別的線程不會修改這個數據,所以不會上鎖,但是在更新的時候會通過標記參數判斷一下在此期間(更新期間)別的線程有沒有已經修改過該標記數據,如果發現有其他線程在修改且未修改完成,並不會像悲觀鎖那樣阻塞線程,而是直接返回,可以去選擇再次重試獲得鎖,也可以直接退出。

舉個流程示例

如CAS操作包括三個操作數:需要讀寫的內存位置(V)、預期原值(A)、新值(B)。如果內存位置與預期原值的A相匹配,說明在此期間(更新期間)別的線程未修改過該標記數據,那麼將內存位置的值更新為新值B。如果內存位置與預期原值的值不匹配,那麼處理器不會做任何操作。

無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)

AQS:

AQS主要利用硬件原語指令CAS,來實現輕量級多線程同步機制,並且不會引起CPU上文切換和調度,同時提供內存可見性和原子化更新保證(線程安全的三要素:原子性、可見性、順序性)。

AQS的本質上是一個同步器/阻塞鎖的基礎框架,其作用主要是提供加鎖、釋放鎖,並在內部維護一個FIFO等待隊列,用於存儲由於鎖競爭而阻塞的線程。

追問1:你認為 ReentrantLock 相比 synchronized 都有哪些區別?

優秀問答摘自:https://ask.csdn.net/questions/1101634

兩者的共同點:

  • 都是用來協調多線程對共享對象、變量的訪問
  • 都是可重入鎖,同一線程可以多次獲得同一個鎖
  • 都保證瞭可見性和互斥性

兩者的不同點:

  • ReentrantLock 顯示的獲得、釋放鎖,synchronized 隱式獲得釋放鎖;
  • ReentrantLock 可響應中斷、可輪回,synchronized 是不可以響應中斷的,為處理鎖的不可用性提供瞭更高的靈活性;
  • ReentrantLock 是API 級別的,synchronized 是 JVM 級別的;
  • ReentrantLock 可以實現公平鎖;
  • ReentrantLock 通過 Condition 可以綁定多個條件;
  • 底層實現不一樣, synchronized 是同步阻塞,使用的是悲觀並發策略,ReentrantLock 是同步非阻塞,采用的是樂觀並發策略;
  • Lock 是一個接口,而 synchronized 是 Java 中的關鍵字,synchronized 是內置的語言實現;
  • synchronized 在發生異常時,會自動釋放線程占有的鎖,因此不會導致死鎖現象發生;
  • 而 Lock 在發生異常時,如果沒有主動通過 unLock()去釋放鎖,則很可能造成死鎖現象,因此使用 Lock 時需要在 finally 塊中釋放鎖。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
  while(條件判斷表達式) {
      condition.wait();
  }
 // 處理邏輯
} finally {
    lock.unlock();
}
  • Lock 可以讓等待鎖的線程響應中斷,而 synchronized 卻不行,使用 synchronized 時,等待的線程會一直等待下去,不能夠響應中斷。
  • 通過 Lock 可以知道有沒有成功獲取鎖,而 synchronized 卻無法辦到。
  • Lock 可以提高多個線程進行讀操作的效率,就是實現讀寫鎖等。

面試題2:解釋一下公平鎖和非公平鎖?

ReenTrantLock可以指定是公平鎖還是非公平鎖,而synchronized隻能是非公平鎖。所謂的公平鎖就是先等待的線程先獲鎖。

我們剛才提及到AQS中,如果線程A正處於lock狀態,線程B進來時發現線程A處於lock狀態,會自動進入阻塞隊列,等待取鎖;這時候當線程C也進來瞭也發現線程A處於lock狀態,也會自動進入阻塞隊列。那麼等A釋放鎖後,下次加鎖到底是線程B先拿到還是線程C先拿到呢?

在這裡插入圖片描述

ReentrantLock有個構造方法用於設置鎖的公平性,如果我們僅僅是new瞭一個ReentrantLock的話,那麼就是非公平鎖(默認),就是靠自己去爭取,完全的隨機性。如果我們在new ReentrantLock(true) 加入 true參數時,公平鎖,就會遵循先入先出的原則,保證瞭鎖的公平性。

面試題3:能詳細說一下CAS具體實現原理麼?

首先,CAS的英文單詞是Compare and Swap,即是比較並替換。CAS機制中使用瞭3個基本操作數:內存地址V,舊的預期值A,待替換的新值B。

CAS規則是:當需要更新一個變量的值的時候,隻有當變量的預期值A和內存地址V中的實際值相同的時候,才會把內存地址V對應的值替換成B。

下面我們通過一個例子來講解:

1.在內存地址V中存儲的值是陳哈哈

在這裡插入圖片描述

2.線程01想要把變量的值改成僑總,對於線程01而言,內存地址 V=‘陳哈哈’,舊的預期值 A=‘陳哈哈’,需要替換的新值 B=‘僑總’。

在這裡插入圖片描述

3.在線程01要提交更新之前,另外一個線程02搶先一步,將內存地址V中的值更新成瞭V=’比特幣’。

在這裡插入圖片描述

4.線程01開始提交更新的時候,按照CAS機制,首先進行A的值與內存地址V中的值進行比較( A=‘陳哈哈’ V=‘比特幣’),發現 A != V 中的實際值,提交失敗。

在這裡插入圖片描述

5.線程01未獲取鎖後進行重試,重新獲取內存地址V的當前值(V=‘比特幣’),並重新賦值想要修改的值(B=‘僑總’)。截至目前,線程01舊的預期值為A=’比特幣’,B=’僑總’,這個重新嘗試的過程被稱為自旋。

在這裡插入圖片描述

6.這一次就比較順利瞭,沒有其他線程改變該變量的值,所以線程01通過CAS機制,比較舊的預期值A與內存地址V的值,相同(V == A),可以替換。

在這裡插入圖片描述

7.線程01進行替換,把地址V(V=‘比特幣’)的值替換成B(B=‘僑總’)。

在這裡插入圖片描述

  以上就是一個比較完整的CAS鎖沖突的處理方式。

  從思想上來看,synchronized屬於悲觀鎖,悲觀的認為程序中的並發問題十分嚴重,所以嚴防死守,隻讓一個線程操作該代碼塊。而CAS屬於樂觀鎖,樂觀地認為程序中的並發問題並不那麼嚴重,所以讓線程不斷的去嘗試更新,在並發問題不嚴重的時候性能要比synchronized快。

追問1:那CAS的缺陷有哪些呢?

當然,CAS也有缺點,如ABA問題,自旋鎖消耗問題、多變量共享一致性問題等。

1.ABA:

問題描述:

線程t1將它的值從A變為B,再從B變為A。同時有線程t2要將值從A變為C。但CAS檢查的時候會發現沒有改變,但是實質上它已經發生瞭改變 。可能會造成數據的缺失。

解決方法:

CAS還是類似於樂觀鎖,同數據樂觀鎖的方式給它加一個版本號或者時間戳,如AtomicStampedReference

2.自旋消耗資源:

問題描述:

多個線程爭奪同一個資源時,如果自旋一直不成功,將會一直占用CPU。

解決方法:

破壞掉for死循環,當超過一定時間或者一定次數時,return退出。JDK8新增的LongAddr,和ConcurrentHashMap類似的方法。當多個線程競爭時,將粒度變小,將一個變量拆分為多個變量,達到多個線程訪問多個資源的效果,最後再調用sum把它合起來。

雖然base和cells都是volatile修飾的,但感覺這個sum操作沒有加鎖,可能sum的結果不是那麼精確。

3.多變量共享一致性問題:

解決方法:

CAS操作是針對一個變量的,如果對多個變量操作,

  • 可以加鎖來解決。
  • 封裝成對象類解決。

追問2:講一下什麼是ABA問題?怎麼解決?

ABA:如果另一個線程修改V值假設原來是A,先修改成B,再修改回成A。當前線程的CAS操作無法分辨當前V值是否發生過變化。

舉個例子1:

例一:你和女神一起喝茶,女神喝瞭一半去廁所瞭,你猥瑣的喝瞭她剩下的半杯,然後又從你杯子裡倒瞭半杯給她,女神回來後也不知道是否被人喝過。

如果覺得例子1太猥瑣的話,請看例子2:

例二:

今天上午10:30:00:我銀行卡有一萬塊錢,今天我來ATM機取5000塊出來買比特幣,但由於ATM機硬件問題,導致取款操作同時提交瞭兩遍,後臺開啟瞭兩個線程(線程1、線程2),兩個線程都是獲取當前值10000元,要更新成5000元;理想情況下,應該一個線程更新成功,一個線程更新失敗,我的存款隻扣除一次,也就是餘額應為5000元 。

好巧不巧,也是今天上午10:30:00:僑總上次買幣欠我5000塊,經過我再三催債,表示再不還錢就把你買幣的事兒告訴你媳婦!也正巧這個點兒,僑總給我轉瞭5000元到卡裡(線程3)。沒想到這再正常不過的事兒,缺被ABA問題坑瞭!我可忍不瞭!

事情是這樣的:

線程1首先執行成功,把餘額10000更新為5000(取錢線程)。同時線程2由於某種原因陷入瞭阻塞狀態(取錢線程)。這時候,(線程3)僑總匯款給瞭我5000元,執行成功,我的賬戶5000更新為10000元。過一會兒,線程2恢復運行,由於之前阻塞的時候獲得瞭當前值為:10000,並且經過compare檢測,此時存款也的確是10000元,所以又成功把變量值從10000更新成瞭5000。僑總:哈哥,5000給你打過去瞭!我:查到賬戶隻有5000,你他娘的糊弄老子呢??僑總:???我好幾十個比特幣的人,還在乎你這5000塊錢瞭。。。

這就是經典的 A → B → A 問題,通過上面的例子,相信同學們也基本瞭解它的原理瞭。其實解決方式也很簡單,比如例一,我們將每一次倒水假設有一個自動記錄儀記錄下,這樣主人回來就可以分辨在她離開後是否發生過重新倒滿的情況。這也是解決ABA問題目前采用的策略。

使用版本號,通過比較值 + 版本號才判斷是否可以替換。這麼看來如果要解決ABA問題,就需要在CAS基礎上在增加一個版本號的校驗,當值 + 版本號都相等時,才進行替換,其他部分均不變。

而在Java中,AtomicStampedReference類就實現瞭用版本號做比較的CAS機制。狗子們,你心裡有數瞭麼?

總結

本篇文章就到這裡瞭,希望能給你帶來幫助,也希望您關註WalkonNet的更多內容!

推薦閱讀: