Java中的鎖分類

1. Java鎖的種類

  • 樂觀鎖/悲觀鎖
  • 獨享鎖/共享鎖
  • 互斥鎖/讀寫鎖
  • 可重入鎖
  • 公平鎖/非公平鎖
  • 分段鎖
  • 偏向鎖/輕量級鎖/重量級鎖
  • 自旋鎖

  以上是一些鎖的名詞,這些分類並不是全是指鎖的狀態,有的指鎖的特性,有的指鎖的設計,下面總結的內容是對每個鎖的名詞進行一定的解釋。

 

1.1 樂觀鎖/悲觀鎖

  樂觀鎖與悲觀鎖並不是特指某兩種類型的鎖,是人們定義出來的概念或思想,主要是指看待併發同步的角度。

  樂觀鎖:顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS(Compare and Swap 比較並交換)實現的。

  悲觀鎖:總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。比如Java裏面的同步原語synchronized關鍵字的實現就是悲觀鎖。

  悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。

  悲觀鎖在Java中的使用,就是利用各種鎖。

  樂觀鎖在Java中的使用,是無鎖編程,常常採用的是CAS算法,典型的例子就是原子類,通過CAS自旋實現原子操作的更新。

1.1.1 樂觀鎖

  樂觀鎖總是認爲不存在併發問題,每次去取數據的時候,總認爲不會有其他線程對數據進行修改,因此不會上鎖。但是在更新時會判斷其他線程在這之前有沒有對數據進行修改,一般會使用“數據版本機制”或“CAS操作”來實現。

(1) 數據版本機制

  實現數據版本一般有兩種,第一種是使用版本號,第二種是使用時間戳。以版本號方式爲例。

  版本號方式:一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。
核心SQL代碼:

 update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};

(2) CAS操作

  CAS(Compare and Swap 比較並交換),當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。

  CAS操作中包含三個操作數——需要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。如果內存位置V的值與預期原值A相匹配,那麼處理器會自動將該位置值更新爲新值B,否則處理器不做任何操作。

1.1.2 悲觀鎖

  悲觀鎖認爲對於同一個數據的併發操作,一定會發生修改的,哪怕沒有修改,也會認爲修改。因此對於同一份數據的併發操作,悲觀鎖採取加鎖的形式。悲觀的認爲,不加鎖併發操作一定會出問題。

  在對任意記錄進行修改前,先嚐試爲該記錄加上排他鎖(exclusive locking)。

  如果加鎖失敗,說明該記錄正在被修改,那麼當前查詢可能要等待或者拋出異常。具體響應方式由開發者根據實際需要決定。

  如果成功加鎖,那麼就可以對記錄做修改,事務完成後就會解鎖了。

  期間如果有其他對該記錄做修改或加排他鎖的操作,都會等待我們解鎖或直接拋出異常。

 

1.2 獨享鎖/共享鎖

  獨享鎖是指該鎖一次只能被一個線程所持有。

  共享鎖是指該鎖可被多個線程所持有。

  對於Java ReentrantLock而言,其是獨享鎖。但是對於Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。

  讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀,寫寫的過程是互斥的。

  獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。

  對於Synchronized而言,當然是獨享鎖。

 

1.3 互斥鎖/讀寫鎖

  上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。

  互斥鎖在Java中的具體實現就是ReentrantLock。

  讀寫鎖在Java中的具體實現就是ReadWriteLock。

 

1.4 可重入鎖

  可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。說的有點抽象,下面會有一個代碼的示例。

  對於Java ReetrantLock而言,從名字就可以看出是一個重入鎖,其名字是Re entrant Lock 重新進入鎖。

  對於Synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖。

synchronized void setA() throws Exception{
  Thread.sleep(1000);
  setB();
}

synchronized void setB() throws Exception{
  Thread.sleep(1000);
}

上面的代碼就是一個可重入鎖的一個特點。如果不是可重入鎖的話,setB可能不會被當前線程執行,可能造成死鎖。

 

1.5 公平鎖/非公平鎖

  公平鎖是指多個線程按照申請鎖的順序來獲取鎖。

  非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能,會造成優先級反轉或者飢餓現象。

  對於Java ReetrantLock而言,通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。

  對於Synchronized而言,也是一種非公平鎖。由於其並不像ReentrantLock是通過AQS的來實現線程調度,所以並沒有任何辦法使其變成公平鎖。

 

1.6 分段鎖

  分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是通過分段鎖的形式來實現高效的併發操作。

  我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱爲Segment,它即類似於HashMap(JDK7和JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。

  當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在哪一個分段中,然後對這個分段進行加鎖,所以當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。

  但是,在統計size的時候,可就是獲取hashmap全局信息的時候,就需要獲取所有的分段鎖才能統計。

  分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。

 

1.7 偏向鎖/輕量級鎖/重量級鎖

  這三種鎖是指鎖的狀態,並且是針對Synchronized。在Java 5通過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是通過對象監視器在對象頭中的字段來表明的。

  偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。降低獲取鎖的代價。

  輕量級鎖是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。

  重量級鎖是指當鎖爲輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓他申請的線程進入阻塞,性能降低。

 

1.8 自旋鎖

  在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。

 

 

發佈了70 篇原創文章 · 獲贊 113 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章