自旋鎖、阻塞鎖、可重入鎖、悲觀鎖、樂觀鎖、讀寫鎖、偏向所、輕量級鎖、重量級鎖、鎖膨脹、物件鎖和類鎖

shuilaner_發表於2018-12-11

1、自旋鎖
自旋鎖可以使執行緒在沒有取得鎖的時候,不被掛起,而轉去執行一個空迴圈,(即所謂的自旋,就是自己執行空迴圈),若在若干個空迴圈後,執行緒如果可以獲得鎖,則繼續執行。若執行緒依然不能獲得鎖,才會被掛起。
使用自旋鎖後,執行緒被掛起的機率相對減少,執行緒執行的連貫性相對加強。因此,對於那些鎖競爭不是很激烈,鎖佔用時間很短的併發執行緒,具有一定的積極意義,但對於鎖競爭激烈,單執行緒鎖佔用很長時間的併發程式,自旋鎖在自旋等待後,往往毅然無法獲得對應的鎖,不僅僅白白浪費了CPU時間,最終還是免不了被掛起的操作 ,反而浪費了系統的資源。
在JDK1.6中,Java虛擬機器提供-XX:+UseSpinning引數來開啟自旋鎖,使用-XX:PreBlockSpin引數來設定自旋鎖等待的次數。
在JDK1.7開始,自旋鎖的引數被取消,虛擬機器不再支援由使用者配置自旋鎖,自旋鎖總是會執行,自旋鎖次數也由虛擬機器自動調整。

可能引起的問題:
1.過多佔據CPU時間:如果鎖的當前持有者長時間不釋放該鎖,那麼等待者將長時間的佔據cpu時間片,導致CPU資源的浪費,因此可以設定一個時間,當鎖持有者超過這個時間不釋放鎖時,等待者會放棄CPU時間片阻塞;
2.死鎖問題:試想一下,有一個執行緒連續兩次試圖獲得自旋鎖(比如在遞迴程式中),第一次這個執行緒獲得了該鎖,當第二次試圖加鎖的時候,檢測到鎖已被佔用(其實是被自己佔用),那麼這時,執行緒會一直等待自己釋放該鎖,而不能繼續執行,這樣就引起了死鎖。因此遞迴程式使用自旋鎖應該遵循以下原則:遞迴程式決不能在持有自旋鎖時呼叫它自己,也決不能在遞迴呼叫時試圖獲得相同的自旋鎖。

2、阻塞鎖
讓執行緒進入阻塞狀態進行等待,當獲得相應的訊號(喚醒,時間) 時,才可以進入執行緒的準備就緒狀態,準備就緒狀態的所有執行緒,通過競爭,進入執行狀態。。
JAVA中,能夠進入\退出、阻塞狀態或包含阻塞鎖的方法有 ,synchronized 關鍵字(其中的重量鎖),ReentrantLock,Object.wait()\notify()

3、可重入鎖
可重入鎖,也叫做遞迴鎖,指的是同一執行緒 外層函式獲得鎖之後 ,內層遞迴函式仍然有獲取該鎖的程式碼,但不受影響。
在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖
下面是使用例項


 
  1. public class Test implements Runnable{

  2.  
  3. public synchronized void get(){

  4. System.out.println(Thread.currentThread().getId());

  5. set();

  6. }

  7.  
  8. public synchronized void set(){

  9. System.out.println(Thread.currentThread().getId());

  10. }

  11.  
  12. @Override

  13. public void run() {

  14. get();

  15. }

  16. public static void main(String[] args) {

  17. Test ss=new Test();

  18. new Thread(ss).start();

  19. new Thread(ss).start();

  20. new Thread(ss).start();

  21. }

  22. }

  23.  
  24. public class Test implements Runnable {

  25. ReentrantLock lock = new ReentrantLock();

  26.  
  27. public void get() {

  28. lock.lock();

  29. System.out.println(Thread.currentThread().getId());

  30. set();

  31. lock.unlock();

  32. }

  33.  
  34. public void set() {

  35. lock.lock();

  36. System.out.println(Thread.currentThread().getId());

  37. lock.unlock();

  38. }

  39.  
  40. @Override

  41. public void run() {

  42. get();

  43. }

  44.  
  45. public static void main(String[] args) {

  46. Test ss = new Test();

  47. new Thread(ss).start();

  48. new Thread(ss).start();

  49. new Thread(ss).start();

  50. }

  51. }


兩個例子最後的結果都是正確的,即 同一個執行緒id被連續輸出兩次。
結果如下:
Threadid: 8
Threadid: 8
Threadid: 10
Threadid: 10
Threadid: 9
Threadid: 9
可重入鎖最大的作用是避免死鎖
我們以自旋鎖作為例子,


 
  1. public class SpinLock {

  2. private AtomicReference<Thread> owner =new AtomicReference<>();

  3. public void lock(){

  4. Thread current = Thread.currentThread();

  5. while(!owner.compareAndSet(null, current)){

  6. }

  7. }

  8. public void unlock (){

  9. Thread current = Thread.currentThread();

  10. owner.compareAndSet(current, null);

  11. }

  12. }


對於自旋鎖來說,
1、若有同一執行緒兩呼叫lock() ,會導致第二次呼叫lock位置進行自旋,產生了死鎖
說明這個鎖並不是可重入的。(在lock函式內,應驗證執行緒是否為已經獲得鎖的執行緒)
2、若1問題已經解決,當unlock()第一次呼叫時,就已經將鎖釋放了。實際上不應釋放鎖。
(採用計數次進行統計)
修改之後,如下:


 
  1. public class SpinLock1 {

  2. private AtomicReference<Thread> owner =new AtomicReference<>();

  3. private int count =0;

  4. public void lock(){

  5. Thread current = Thread.currentThread();

  6. if(current==owner.get()) {

  7. count++;

  8. return ;

  9. }

  10. while(!owner.compareAndSet(null, current)){

  11. }

  12. }

  13. public void unlock (){

  14. Thread current = Thread.currentThread();

  15. if(current==owner.get()){

  16. if(count!=0){

  17. count--;

  18. }else{

  19. owner.compareAndSet(current, null);

  20. }

  21. }

  22. }

  23. }

 

該自旋鎖即為可重入鎖。

4 悲觀鎖和樂觀鎖
悲觀鎖(Pessimistic Lock), 顧名思義就是很悲觀,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會block直到它拿到鎖。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。獨佔鎖是悲觀鎖的一種實現

樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制。樂觀鎖適用於多讀的應用型別,這樣可以提高吞吐量,像資料庫如果提供類似於write_condition機制的其實都是提供的樂觀鎖。使用CAS來保證,保證這個操作的原子性

兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下,即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常產生衝突,上層應用會不斷的進行retry,這樣反倒是降低了效能,所以這種情況下用悲觀鎖就比較合適。

參考:http://www.cnblogs.com/softidea/p/5309312.html
http://blog.csdn.net/hongchangfirst/article/details/26004335

5 輪詢鎖和定時鎖
由tryLock實現,與無條件獲取鎖模式相比,它們具有更完善的錯誤恢復機制。可避免死鎖的發生:
boolean tryLock():僅在呼叫時鎖為空閒狀態才獲取該鎖。如果鎖可用,則獲取鎖,並立即返回值 true。如果鎖不可用,則此方法將立即返回值 false。
 
boolean tryLock(long time, TimeUnit unit) throws InterruptedException:
  如果鎖在給定的等待時間內空閒,並且當前執行緒未被中斷,則獲取鎖。
 
  如果鎖可用,則此方法將立即返回值 true。如果鎖不可用,出於執行緒排程目的,將禁用當前執行緒,並且在發生以下三種情況之一前,該執行緒將一直處於休眠狀態:
 
  鎖由當前執行緒獲得;或者
  其他某個執行緒中斷當前執行緒,並且支援對鎖獲取的中斷;或者
  已超過指定的等待時間
  如果獲得了鎖,則返回值 true。
 
  如果當前執行緒:
 
  在進入此方法時已經設定了該執行緒的中斷狀態;或者
  在獲取鎖時被中斷,並且支援對鎖獲取的中斷,
  則將丟擲 InterruptedException,並會清除當前執行緒的已中斷狀態。
  如果超過了指定的等待時間,則將返回值 false。如果 time 小於等於 0,該方法將完全不等待。

6 顯示鎖和內建鎖
顯示鎖用Lock來定義、內建鎖用syschronized。
內建鎖:每個java物件都可以用做一個實現同步的鎖,這些鎖成為內建鎖。執行緒進入同步程式碼塊或方法的時候會自動獲得該鎖,在退出同步程式碼塊或方法時會釋放該鎖。獲得內建鎖的唯一途徑就是進入這個鎖的保護的同步程式碼塊或方法。
內建鎖是互斥鎖。

7 讀-寫鎖
Lock介面以及物件,使用它,很優雅的控制了競爭資源的安全訪問,但是這種鎖不區分讀寫,稱這種鎖為普通鎖。為了提高效能,Java提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是無阻塞的,在一定程度上提高了程式的執行效率。
Java中讀寫鎖有個介面java.util.concurrent.locks.ReadWriteLock,也有具體的實現ReentrantReadWriteLock,詳細的API可以檢視JavaAPI文件。
ReentrantReadWriteLock 和 ReentrantLock 不是繼承關係,但都是基於 AbstractQueuedSynchronizer 來實現。
lock方法 是基於CAS 來實現的
ReadWriteLock中暴露了兩個Lock物件:

在讀寫鎖的加鎖策略中,允許多個讀操作同時進行,但每次只允許一個寫操作。讀寫鎖是一種效能優化的策略。

RentrantReadWriteLock在構造時也可以選擇是一個非公平的鎖(預設)還是公平的鎖。

8 物件鎖和類鎖
java的物件鎖和類鎖在鎖的概念上基本上和內建鎖是一致的,但是,兩個鎖實際是有很大的區別的,物件鎖是用於物件例項方法,或者一個物件例項上的,類鎖是用於類的靜態方法或者一個類的class物件上的。
類的物件例項可以有很多個,但是每個類只有一個class物件,所以不同物件例項的物件鎖是互不干擾的,但是每個類只有一個類鎖。但是有一點必須注意的是,其實類鎖只是一個概念上的東西,並不是真實存在的,它只是用來幫助我們理解鎖定例項方法和靜態方法的區別的.
synchronized只是一個內建鎖的加鎖機制,當某個方法加上synchronized關鍵字後,就表明要獲得該內建鎖才能執行,並不能阻止其他執行緒訪問不需要獲得該內建鎖的方法。

呼叫物件wait()方法時,會釋放持有的物件鎖,以便於呼叫notify方法使用。notify()呼叫之後,會等到notify所在的執行緒執行完之後再釋放鎖


9:鎖粗化(Lock Coarsening):
鎖粗化的概念應該比較好理解,就是將多次連線在一起的加鎖、解鎖操作合併為一次,將多個連續的鎖擴充套件成一個範圍更大的鎖。舉個例子:


 
  1. 1 package com.paddx.test.string;

  2. 2

  3. 3 public class StringBufferTest {

  4. 4 StringBuffer stringBuffer = new StringBuffer();

  5. 5

  6. 6 public void append(){

  7. 7 stringBuffer.append("a");

  8. 8 stringBuffer.append("b");

  9. 9 stringBuffer.append("c");

  10. 10 }

  11. 11 }


  這裡每次呼叫stringBuffer.append方法都需要加鎖和解鎖,如果虛擬機器檢測到有一系列連串的對同一個物件加鎖和解鎖操作,就會將其合併成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。

10 互斥鎖
互斥鎖, 指的是一次最多隻能有一個執行緒持有的鎖。如Java的Lock


15 無鎖狀態-》偏向鎖-》輕量級鎖-》重量級鎖。鎖膨脹
 鎖的狀態
總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。JDK 1.6中預設是開啟偏向鎖和輕量級鎖的,
鎖膨脹:從輕量鎖膨脹到重量級鎖是在輕量級鎖解鎖過程發生的。
重量級鎖:Synchronized是通過物件內部的一個叫做監視器鎖(monitor)來實現的。但是監視器鎖本質又是依賴於底層的作業系統的Mutex Lock來實現的。而作業系統實現執行緒之間的切換這就需要從使用者態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什麼Synchronized效率低的原因。因此,這種依賴於作業系統Mutex Lock所實現的鎖我們稱之為“重量級鎖”。
輕量級鎖:“輕量級”是相對於使用作業系統互斥量來實現的傳統鎖而言的。但是,首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用產生的效能消耗。在解釋輕量級鎖的執行過程之前,先明白一點,輕量級鎖所適應的場景是執行緒交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。
偏向鎖: 引入偏向鎖是為了在無多執行緒競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由於一旦出現多執行緒競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的效能損耗必須小於節省下來的CAS原子指令的效能消耗)。上面說過,輕量級鎖是為了線上程交替執行同步塊時提高效能,而偏向鎖則是在只有一個執行緒執行同步塊時進一步提高效能。

無鎖狀態:在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態。

重量級鎖、輕量級鎖和偏向鎖之間轉換:

11 鎖消除(Lock Elimination):鎖消除即刪除不必要的加鎖操作。根據程式碼逃逸技術,如果判斷到一段程式碼中,堆上的資料不會逃逸出當前執行緒,那麼可以認為這段程式碼是執行緒安全的,不必要加鎖。看下面這段程式:
 


 
  1. package com.paddx.test.concurrent;

  2.  
  3. public class SynchronizedTest02 {

  4.  
  5. public static void main(String[] args) {

  6. SynchronizedTest02 test02 = new SynchronizedTest02();

  7. //啟動預熱

  8. for (int i = 0; i < 10000; i++) {

  9. i++;

  10. }

  11. long start = System.currentTimeMillis();

  12. for (int i = 0; i < 100000000; i++) {

  13. test02.append("abc", "def");

  14. }

  15. System.out.println("Time=" + (System.currentTimeMillis() - start));

  16. }

  17.  
  18. public void append(String str1, String str2) {

  19. StringBuffer sb = new StringBuffer();

  20. sb.append(str1).append(str2);

  21. }

  22. }


雖然StringBuffer的append是一個同步方法,但是這段程式中的StringBuffer屬於一個區域性變數,並且不會從該方法中逃逸出去,所以其實這過程是執行緒安全的,可以將鎖消除。下面是我本地執行的結果
 

12、訊號量
執行緒同步工具:Semaphore

http://ifeve.com/java_lock_see/
http://www.cnblogs.com/paddix/p/5405678.html
http://www.cnblogs.com/softidea/p/5530761.html

相關文章