大數據

Java 之經典鎖 Lock ——《我的Java打怪日記》

Doug Lea 大牛在 JDK1.5 併發包 java.util.concurrent.locks 中增加了新的併發編程接口 Lock (以及相關實現類)。Lock 提供了與 synchronized 關鍵字類似的同步功能,但需要在使用時手動獲取和釋放鎖。

Lock 接口方法

  • void lock():如果鎖已被其他線程佔用,則進行等待,異常時不會自動釋放鎖
  • boolean tryLock():如果當前鎖沒有被其他線程佔用,則獲取成功,返回 true;否則獲取鎖失敗,返回 false
  • boolean tryLock(long time, TimeUnit unit):獲取鎖超時就放棄
  • void lockInterruptibly():在等待鎖的過程中,允許中斷
  • void unlock():解鎖
  • Condition newCondition()

Lock 實現類

ReentrantLock

ReentrantLock 主要利用 CAS + AQS 隊列來實現。

CAS

Compare and Swap,比較並交換。CAS 有3個操作數:內存值V、預期值A、要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什麼都不做。該操作是一個原子操作,被廣泛的應用在 Java 的底層實現中,主要是由sun.misc.Unsafe 這個類通過 JNI 調用 CPU 底層指令實現。

StampedLock

StampedLock 是為了優化可重入讀寫鎖性能的一個鎖實現工具,JDK8 開始引入。

相比於普通的 ReentranReadWriteLock 多了一種樂觀讀的功能,在 API 上增加了 stamp 的入參和返回值。

不支持重入。

樂觀讀鎖

  1. 進入悲觀讀鎖前先看下有沒有進入寫模式(即有沒有已經獲取了悲觀寫鎖)
  2. 如果其他線程已經獲取了悲觀寫鎖,那麼就只能老老實實的獲取悲觀讀鎖(這種情況相當於退化成了讀寫鎖)
  3. 如果其他線程沒有獲取悲觀寫鎖,那麼就不用獲取悲觀讀鎖了,減少了一次獲取悲觀讀鎖的消耗和避免了因為讀鎖導致寫鎖阻塞的問題,直接返回讀的數據即可(必須在 tryOptimisticRead 和 validate 之間獲取好數據,否則數據可能會不一致了,試想如果過了 validate 再獲取數據,這時數據可能被修改並且讀操作也沒有任何保護措施)

鎖的分類

image.png

樂觀鎖和悲觀鎖

  • 悲觀鎖:對數據被外界修改持保守態度,認為數據很容易就會被其他線程修改,所以在數據被處理前先對數據進行加鎖,並在整個數據處理過程中,使數據處於鎖定狀態。悲觀鎖的實現往往依靠數據庫提供的鎖機制,即在數據庫中,在對數據記錄操作前給記錄加排它鎖。如果獲取鎖失敗,則說明數據正在被其他線程修改,當前線程則等待或者拋出異常。如果獲取鎖成功,則對記錄進行操作,然後提交事務後釋放排它鎖。
  • 樂觀鎖:相對悲觀鎖來說的,它認為數據在一般情況下不會造成衝突,所以在訪問記錄前不會加排它鎖,而是在進行數據提交更新時,才會正式對數據衝突與否進行檢測。樂觀鎖並不會使用數據庫提供的鎖機制,一般在表中添加 version 字段或者使用業務狀態來實現。樂觀鎖直到提交時才鎖定,所以不會產生任何死鎖。

獨佔鎖與共享鎖

根據鎖只能被單個線程持有還是能被多個線程共同持有,鎖可以分為獨佔鎖和共享鎖。

  • 獨佔鎖:保證任何時候都只有一個線程能得到鎖,ReentrantLock 就是以獨佔方式實現的。獨佔鎖是一種悲觀鎖,由於每次訪問資源都先加上互斥鎖,這限制了併發性,因為讀操作並不會影響數據的一致性,而獨佔鎖只允許在同一時間由一個線程讀取數據,其他線程必須等待當前線程釋放鎖才能進行讀取。
  • 共享鎖:可以同時由多個線程持有,例如 ReadWriteLock 讀寫鎖,它允許一個資源可以被多線程同時進行讀操作。共享鎖則是一種樂觀鎖,它放寬了加鎖的條件,允許多個線程同時進行讀操作。

公平鎖與非公平鎖

根據線程獲取鎖的搶佔機制,鎖可以分為公平鎖和非公平鎖,公平鎖表示線程獲取鎖的順序是按照線程請求鎖的時間早晚來決定的,也就是最早請求鎖的線程將最早獲取到鎖。而非公平鎖則在運行時闖入,也就是先來不一定先得。

在沒有公平性需求的前提下儘量使用非公平鎖,因為公平鎖會帶來性能開銷。

ReentrantLock 提供了公平和非公平鎖的實現。

  • 公平鎖:ReentrantLock pairLock = new ReentrantLock(true)
  • 非公平鎖:ReentrantLock pairLock = new ReentrantLock(false)如果構造函數不傳遞參數,則默認是非公平鎖。

可重入鎖

當一個線程再次獲取它自己已經獲取的鎖時,如果不被阻塞,即該鎖是可重入的,也就是隻要該線程獲取了該鎖,那麼可以進入被該鎖鎖住的代碼。

synchronized 內部鎖是可重入鎖。可重入鎖的原理是在鎖內部維護一個線程標識,用來標識該鎖目前被哪個線程佔用,然後關聯一個計數器。一開始計數器值為0,說明該鎖沒有被任何線程佔用。當一個線程獲取了該鎖時,計數器的值會變成1,這時其他線程再來獲取該鎖時會發現鎖的所有者不是自己而被阻塞掛起。但是當獲取了該鎖的線程再次獲取鎖時發現鎖擁有者是自己,就會把計數器值加+1,當釋放鎖後計數器值-1。當計數器值為0時,鎖裡面的線程標示被重置為null,這時候被阻塞的線程會被喚醒來競爭獲取該鎖。

自旋鎖

由於 Java 中的線程是與操作系統中的線程一一對應的,所以當一個線程在獲取鎖(比如獨佔鎖)失敗後,會被切換到內核狀態而被掛起。當該線程獲取到鎖時又需要將其切換到內核狀態而喚醒該線程。而從用戶狀態切換到內核狀態的開銷是比較大的,在一定程度上會影響併發性能。

  • 自旋鎖:當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出循環。Atomic 包下類的 CAS 原理即自旋鎖。

優點:自旋鎖不會使線程狀態發生切換,一直處於用戶態,即線程一直都是 active 的;不會使線程進入阻塞狀態,減少了不必要的上下文切換,執行速度快。

缺點:如果某個線程持有鎖的時間過長,就會導致其它等待獲取鎖的線程進入循環等待,消耗 CPU。使用不當會造成 CPU 使用率極高。不公平的鎖就會存在“線程飢餓”問題。

鎖的狀態

鎖的狀態是通過對象監視器在對象頭中的字段來表明的。

四種狀態會隨著競爭的情況逐漸升級,而且是不可逆的過程,即不可降級。

這四種狀態都不是Java語言中的鎖,而是 JVM 為了提高鎖的獲取與釋放效率而做的優化(使用synchronized時)。

  1. 無鎖狀態
  2. 偏向鎖狀態:指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖,降低獲取鎖的代價。
  3. 輕量級鎖狀態:指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋嘗試獲取鎖,不會阻塞,提高性能。
  4. 重量級鎖狀態:指當鎖為輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低。

Leave a Reply

Your email address will not be published. Required fields are marked *