Java鎖常見知識點

synchronized實現原理

  •  synchronized同步代碼塊:synchronized關鍵字經過編譯之後,會在同步代碼塊前後分別形成monitorerter和monitorexit字節碼指令,在執行monitorenter指令的時候,首先嚐試獲取對象的鎖,如果這麼鎖沒有被鎖定或者當前線程已經擁有了那個對象的鎖,鎖的計數器就加1,在執行monitorexit指令時會將鎖的計數器減1,當減爲0的時候就釋放鎖。如果獲取鎖對象一直失敗,那當前線程就阻塞等待,直到對象鎖被另一個線程釋放爲止。
  • synchronized同步方法:方法級的同步是隱式的,無需通過字節碼指令來控制,JVM可以從方法常量池的方法表結構中的ACC_SYNCHRONIZED訪問標誌得知一個方法是否聲明爲同步方法。當方法調用時,調用指令會檢查方法中的ACC_SYNCHRONIZED訪問標誌是否被設置,如果設置了,執行線程就要求先持有monitor對象,然後才能執行方法,最後當方法執行完(無論正常完成還是非正常完成)時釋放monitor對象。在方法執行期間,執行線程持有了管程,其他線程都無法再次獲取同一個管程。

☞ Synchronized優化

Java6之後,爲了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了偏向鎖、輕量級鎖和自旋鎖等概念。

ReentrantLock是如何實現可重入性的?

內部自定了同步器Sync,加鎖的時候通過CAS算法,將線程對象放到一個雙向鏈表中,每次獲取鎖的時候,看下當前維護的那個線程ID和當前請求的線程ID是否一樣,一樣就可重入了。

ReentrantLock如何避免死鎖? 

  • 響應中斷lockInterruptibly()
  • 可輪詢鎖tryLock()
  • 定時鎖tryLock(long time)

tryLock, lock, lockInterruptibly的區別

  1. tryLock能獲得鎖就返回true,不能就立即返回false
  2. tryLock(long timeout, TimeUnit unit),可以增加時間限制,如果超過該時間段還沒獲得鎖就返回false
  3. lock能獲得鎖就返回true,不能的話就一直等待獲得鎖
  4. lock和lockInterruptibly,如果兩個線程分別執行這兩個方法,但此時中斷這兩個線程,lock不會拋出異常,而lockInterruptibly會拋出異常。

CountDownLatch和CyclicBarrier的區別是什麼

 CountDownLatch是等待其他線程執行到某一個點的時候,再繼續執行邏輯(子線程不會被阻塞,會繼續執行),只能使用一次。最常見的是join形式,主線程等待子線程執行完任務,在用主線程去獲取結果的方式,內部是用計數器相減實現的,AQS的state承擔了計數器的作用,初始化的時候,使用AQS賦值,主線程調用await(),被加入共享線程等待隊列裏面,子線程調用countDown的時候,使用自選的方式,減1,直到爲0時觸發喚醒。

CyclicBarrier迴環屏障,主要是等待一組線程到同一個狀態的時候放閘同時啓動。CyclicBarrier還可以傳遞一個Runnable對象,可以到放閘的時候執行這個任務,CyclicBarrier是可循環的,當調用await的時候如果count變成了0則會充值狀態。CyclicBarrier新增了一個字段parties,用來保存初始值,當count變爲0的時候就重新賦值。還有一個不同點,CyclicBarrier不是基於AQS的,而是基於ReentrantLock實現的,存放的等待隊列使用了條件變量的方式。

synchronized與ReentrantLock的區別

  • 都是可重入鎖:ReentrantLock是顯式獲取和釋放鎖,synchronized是隱式的
  • ReentrantLock可以直到有沒有成功獲取鎖,可以定義讀寫鎖,是api級別,synchronized是JVM級別
  • ReentrantLock可以定義公平鎖,Lock是接口,synchronized是Java關鍵字

什麼是信號量Semaphore

信號量是一種固定資源的限制的一種併發工具包,基於AQS實現,在構造的時候會設置一個值,代表着資源數量。信號量主要是應用於多個共享資源的互斥使用,以及用於併發線程數的控制。信號量也分公平和非公平的情況,基本方式和ReentrantLock差不多,在請求資源調用task時,會用自旋的方式減1,如果成功,則獲取成功了,如果失敗,導致資源數變爲了0,就會加入隊列裏面等待;調用release的時候會加1,補充資源,並喚醒等待隊列。

Samaphore應用

  • acquire(), release()可用於對象池、資源池的構建,比如靜態全局對象池,數據庫連接池
  • 可創建計數爲1的Samaphore,作爲互斥鎖(二元信號量)

可重入鎖概念

  1. 可重入鎖是指同一個線程可以多次獲取同一把鎖,不會因爲之前已經獲取過還沒釋放而阻塞
  2. ReentrantLock和synchronized都是可重入鎖
  3. 可重入鎖的一個優點是可以一定程度避免死鎖 

ReentranLock原理

☞ CAS+AQS隊列來實現

  1. 先通過CAS嘗試獲取鎖,如果此時已經有線程佔據了鎖,那就加入AQS隊列並且被掛起
  2. 當鎖被釋放之後,排在隊首的線程會被喚醒CAS再次嘗試獲取鎖
  3. 如果是非公平鎖,同時還有一個線程進來嘗試獲取可能會讓這個線程搶到鎖
  4. 如果是公平鎖,新加的線程會排到隊尾,由隊首的線程獲取到鎖

AQS原理

Node內部類構成一個雙向鏈表結構的同步隊列,通過控制state (volatile的int類型)狀態來判斷鎖的狀態.(1)對於非可重入鎖狀態不是0則去阻塞;(2)對於可重入鎖如果是0則執行,非0則判斷當前線程是否是獲取到這個鎖的線程,是的話把state狀態+1,比如重入5次,那麼state=5,而在釋放鎖的時候,同樣需要釋放5次直到state=0其他線程纔有資格獲得鎖。

AQS兩種資源共享方式

  •  Exclusive:獨佔,只有一個線程能執行,如ReentrantLock
  • Share:共享,多個線程可以同時執行,如Semaphore, CountDownLotch, ReadWriteLock, CyclicBarrier

CAS原理、缺點及應用

☞ 原理

內存值V,舊的期望值A,要修改的新值B,當A=V時,將內存值修改爲B,否則什麼都不做。

☞ 缺點

  1. ABA問題
  2. 如果CAS失敗,自旋會給CPU帶來壓力
  3. 只能保證對一個變量的原子性操作,i++這種是不能保證的

☞ 應用

Atomic系列

公平鎖和非公平鎖

  1. 公平鎖在分配鎖前檢查是否有線程在排隊等待獲取該鎖,優先分配排隊時間最常的線程,非公平鎖直接嘗試獲取鎖
  2. 公平鎖需要多維護一個線程隊列,效率低,默認非公平

獨佔鎖和共享鎖

  1. ReentrantLock爲獨佔鎖(悲觀加鎖策略)
  2. ReentrantReadWriterLock中讀鎖爲共享鎖

4種鎖狀態

  1. 無鎖
  2. 偏向鎖 - 會偏向第一個訪問鎖的線程,當一個線程訪問同步塊代碼獲得鎖時,會在對象頭和棧幀記錄裏存儲鎖偏向的線程ID,當這個線程再次進入同步代碼塊時,就不需要CAS操作來加鎖了,只要測試一下對象頭裏面是否存儲指向當前線程的偏向鎖,如果偏向鎖未啓動,new出的對象是普通對象(即無鎖,有稍微競爭會造成輕量級鎖),如果啓動,new出的對象是匿名偏向(偏向鎖)。對象頭主要包括兩部分數據:Mark Work(標記字段,存儲對象自身的運行時數據)、class pointer(類型指針,對象指向它的類元數據的指針)
  3. 輕量級鎖(自旋鎖)
    1. 在把線程進行阻塞操作之前先讓線程自旋等待一段時間,可能在等待期間其他線程已經解鎖,這是就無需再進行阻塞操作,避免了用戶態到內核態的切換(自適應自選時間爲一個線程上下文切換時間)
    2. 在用自旋鎖時有可能會造成死鎖,當遞歸調用時有可能造成死鎖
    3. 自旋鎖底層是通過指向線程棧中Lock Record的指針來實現
  4. 重量級鎖

輕量級鎖與偏向鎖的區別

  1. 輕量級鎖是通過CAS來避免進入開銷較大的互斥操作
  2. 偏向鎖是在無競爭場景下完全消除同步,連CAS也不執行

自旋鎖升級到重量級鎖條件

  1. 某線程自旋次數超過10次
  2. 等待的自旋線程超過了系統core數的一半

讀寫鎖及其實現方式

常用的讀寫鎖ReentrantReadWriteLock,這個其實和ReentrantLock相似,也是基於AQS的,但是這個是基於共享資源的,不是互斥,關鍵在於state的處理,讀寫鎖把高16位記爲讀狀態,低16位記爲寫狀態,就分開了,讀讀情況是讀鎖重入,讀寫/寫讀/寫寫都是互斥的,只要判斷低16位就好了。

zookeeper實現分佈式鎖

  1.  利用節點名稱唯一性來實現,加鎖時所有客戶端一起創建節點,只有一個創建成功者獲得鎖,解鎖時刪除節點。
  2. 利用臨時順序節點來實現,加鎖時所有客戶端都創建臨時順序節點,創建節點序號最小的獲得鎖,否則監視比機子序號次小的節點進行等待
  3. 方法2相比方案1的好處時當zookeeper宕機後,臨時順序節點會自動刪除釋放鎖,不會噪聲鎖等待
  4. 方案1會產生驚羣效應(當有很多進程在等待鎖的時候,在釋放鎖的時候會有很多進程就過來爭奪鎖)
  5. 由於需要頻繁創建和刪除節點,性能上不如redis鎖

volatile變量

  1. 變量可見性
  2. 防止指令重排序
  3. 保障變量單次讀寫的原子性,但不能保證i++這種操作的原子性,因爲本質是讀,寫兩次操作

volatile如何保證線程間可見和避免指令重排

volatile之可見性是由原子性保證的,在jmm中定義了8類原子性指令,比如write, store, read, load。而volatile就要求write-store, load-read成爲一個原子性操作,這樣子可以確保在讀取的時候都是從主內存讀入,寫入的時候會同步到主內存中。

指令重排則是由內存屏障來保證的,有兩個內存屏障:

  1. 一個是編譯器屏障:阻止編譯器重排,保證編譯程序時在優化屏障之後的指令不會在優化屏障之後執行
  2. 二是cput屏障:sfence保證寫入,lfence保證讀取,lock類似於鎖的方式。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章