聊聊併發(七)——鎖

L發表於2021-12-09

一、樂觀鎖和悲觀鎖

1、樂觀鎖

  樂觀鎖只是一種設計思想,並不是真的有一種鎖是樂觀的。
  思想:每次操作共享資料之前,都認為其他執行緒不會修改資料,所以都不獲取鎖,直接操作。只在最後更新的時候會判斷一下在此期間是否有其他執行緒更新過這個資料。其實是一種無鎖狀態的更新。
  典型實現:資料庫版本號;CAS演算法。

2、悲觀鎖

  悲觀鎖只是一種設計思想,並不是真的有一種鎖是悲觀的。
  思想:每次操作共享資料之前,都認為其他執行緒會修改資料,所以都先獲取鎖,才操作。未獲得鎖的執行緒,必須阻塞等待。
  典型實現:synchronized;ReentrantLock。

二、共享鎖和排他鎖

1、介紹

  對資料的訪問通常分為兩種情況,讀(查詢)和寫(新增、修改、刪除)。
  多個執行緒併發讀資料,是不會出現問題的。但是,多個執行緒併發寫資料,到底是寫入哪個執行緒的資料呢?這就是平時所說的執行緒同步問題。
  所以,寫寫/讀寫需要互斥訪問,讀讀不需要互斥訪問。

2、排他鎖(寫鎖)

  排他鎖(X鎖),又稱寫鎖、獨佔鎖、互斥鎖:鎖一次只能被一個執行緒所持有。如果一個執行緒對資料加上排他鎖後,那麼其他執行緒不能再對該資料加任何型別的鎖。獲得排他鎖的執行緒即能讀資料又能修改資料。
  理解:一個執行緒獲取寫鎖,其對資料可讀,可寫。其他執行緒只能等待,讀,寫都不可以。即:寫寫/讀寫需要互斥訪問。
  顯然:synchronized 和 Lock 的實現類就是排他鎖。

3、共享鎖(讀鎖)

  共享鎖(S鎖),又稱讀鎖:一種只讀的資料鎖,可被多個執行緒所持有。如果一個執行緒對資料加上共享鎖後,那麼其他執行緒只能對資料再加共享鎖,不能加排他鎖。獲得共享鎖的執行緒只能讀資料,不能修改資料。
  理解:一個執行緒獲取讀鎖,其對資料只可讀,不可寫。其他執行緒可以再獲取讀鎖,但不可獲取寫鎖。即:讀寫需要互斥訪問,讀讀不需要互斥訪問。

4、應用

  問題:若對一個共享資料,加了 synchronized 排他鎖(互斥鎖),而對資料的訪問又僅僅只是讀。那麼,勢必會影響讀的效率。
  原因:一次只能被一個執行緒訪問,未獲取到鎖的執行緒則必須等待。即:A讀完,B才能讀,B讀完,C才能讀。

  解決:可以用讀寫鎖來提高效率。在 JUC 包中,ReadWriteLock 就維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。只要沒有 writer,讀鎖可以由多個 reader 執行緒同時保持。讀寫鎖相比於互斥鎖併發程度更高,每次只有一個寫執行緒,但是可以同時有多個執行緒併發讀。
  讀鎖,可以多個執行緒併發的持有。
  寫鎖,是獨佔的。
  原始碼示例:讀寫鎖

1 public interface ReadWriteLock {
2     // 返回一個讀鎖(共享鎖)
3     Lock readLock();
4 
5     // 返回一個寫鎖(排他鎖)
6     Lock writeLock();
7 }

  這也是,synchronize與Lock的異同之一。

三、公平鎖和非公平鎖

1、介紹

  公平鎖:多個執行緒獲取鎖的順序,是按照它們發出請求的順序來的。
  非公平鎖:多個執行緒獲取鎖的順序,是隨機的。誰搶到是誰的。

2、比較

  效率:顯然,非公平鎖,效率高;公平鎖,效率相對低。
  問題:非公平鎖,大家自己搶鎖,會導致一些一直搶不到鎖的執行緒餓死(執行緒飢餓:執行緒因長時間得不到CPU執行權,導致一直得不到執行的現象);公平鎖,可以保證所有的執行緒都會得到執行。
  典型實現:synchronized 是非公平鎖。Lock 預設是非公平鎖,也可以通過構造器引數 new 一個公平鎖。
  原始碼示例:ReentrantLock 構造器

1 // 預設構造器是 new 一個非公平鎖.
2 public ReentrantLock() {
3     sync = new NonfairSync();
4 }
5 
6 // 根據引數確定建立公平鎖還是非公平鎖.
7 public ReentrantLock(boolean fair) {
8     sync = fair ? new FairSync() : new NonfairSync();
9 }

3、演示

  程式碼示例:公平鎖與非公平鎖

 1 // 不寫註釋也能看懂的程式碼
 2 public class Main {
 3     public static void main(String[] args) {
 4         final LockDemo lockDemo = new LockDemo();
 5         Thread thread1 = new Thread(lockDemo, "執行緒A");
 6         Thread thread2 = new Thread(lockDemo, "執行緒B");
 7         Thread thread3 = new Thread(lockDemo, "執行緒C");
 8 
 9         thread1.start();
10         thread2.start();
11         thread3.start();
12     }
13 }
14 
15 class LockDemo implements Runnable {
16 
17     // 這裡使用的是 非公平鎖
18     private final ReentrantLock lock = new ReentrantLock();
19 
20     @Override
21     public void run() {
22         while (true) {
23             lock.lock();
24 
25             try {
26                 System.out.println(Thread.currentThread().getName() + " 獲取到了鎖~");
27             } finally {
28                 lock.unlock();
29             }
30         }
31     }
32 }
33 
34 // 非公平鎖:結果(擷取一部分)
35 執行緒A 獲取到了鎖~
36 執行緒A 獲取到了鎖~
37 執行緒A 獲取到了鎖~
38 執行緒A 獲取到了鎖~
39 執行緒A 獲取到了鎖~
40 執行緒A 獲取到了鎖~
41 執行緒C 獲取到了鎖~
42 執行緒C 獲取到了鎖~
43 執行緒C 獲取到了鎖~
44 執行緒C 獲取到了鎖~
45 
46 
47 // 修改為公平鎖
48 private final ReentrantLock lock = new ReentrantLock(true);
49 
50 // 公平鎖:結果(擷取一部分)
51 執行緒A 獲取到了鎖~
52 執行緒B 獲取到了鎖~
53 執行緒C 獲取到了鎖~
54 執行緒A 獲取到了鎖~
55 執行緒B 獲取到了鎖~
56 執行緒C 獲取到了鎖~

  可以發現:非公平鎖,獲取鎖是隨機的。公平鎖,獲取鎖順序是依次的,ABC,或者BCA,或者CAB。

四、可重入鎖(遞迴鎖)

  可重入鎖,又稱遞迴鎖,是指同一個執行緒在外層方法獲取了鎖,再進入內層方法會自動獲取鎖。

  典型實現:synchronized 和 ReentrantLock 都是可重入鎖。可重入鎖的好處是可一定程度避免死鎖。

1、設計可重入鎖

  程式碼示例:一種可重入鎖

聊聊併發(七)——鎖
 1 public class Lock {
 2 
 3     // 是否被鎖
 4     private boolean locked = false;
 5 
 6     // 當前持有鎖的執行緒
 7     private Thread ownerThread;
 8 
 9     // 鎖狀態標誌
10     private int state;
11 
12     public synchronized void lock() throws Exception {
13         final Thread thread = Thread.currentThread();
14         while (locked && ownerThread != thread) {
15             wait();
16         }
17 
18         locked = true;
19         // 每重入一次 標誌 +1
20         state++;
21         ownerThread = thread;
22     }
23 
24     public synchronized void unLock() {
25         if (Thread.currentThread() == this.ownerThread) {
26             state--;
27             // 表示完全釋放鎖
28             if (state == 0) {
29                 locked = false;
30                 notify();
31             }
32         }
33     }
34 }
一種可重入鎖

2、設計不可重入鎖

  不可重入鎖,即若當前執行緒執行某個方法已經獲取了該鎖,那麼在方法中嘗試再次獲取鎖時,會因獲取不到而阻塞。
  程式碼示例:一種不可重入鎖

 1 public class Lock {
 2 
 3     // 是否被鎖
 4     private boolean locked = false;
 5 
 6     public synchronized void lock() throws Exception {
 7         // 如果已經被鎖了,就等待
 8         while (locked) {
 9             wait();
10         }
11 
12         locked = true;
13     }
14 
15     public synchronized void unLock() {
16         locked = false;
17         notify();
18     }
19 }

五、自旋鎖

1、介紹

  並不是一把鎖,也只是一種思想。所謂自旋,就是失敗了,不斷嘗試。執行緒並不是被直接阻塞,而是執行一個忙迴圈,這個過程叫自旋。

  對CAS演算法不瞭解的,可以先看這篇CAS演算法

2、優缺點

  優點:減少執行緒被掛起的機率,執行緒的掛起和喚醒也需要消耗資源。
  缺點:若一個執行緒佔用的時間比較長,導致其他執行緒一直失敗,一直迴圈,忙迴圈浪費系統資源,就會降低整體效能。因此自旋鎖是不適應鎖佔用時間長的併發情況的。

3、手寫一個自旋鎖

  程式碼示例:手寫一個自旋鎖

 1 public class SpinLock {
 2 
 3     AtomicReference<Thread> lock = new AtomicReference<>();
 4 
 5     // 上鎖
 6     public void lock() throws Exception {
 7         final Thread t = Thread.currentThread();
 8 
 9         // 通過CAS算數將 null --> 當前執行緒. 成功表示獲取到鎖. 否則自旋
10         while (!lock.compareAndSet(null, t)) {
11 
12         }
13     }
14 
15     // 釋放鎖
16     public void unLock() {
17         final Thread t = Thread.currentThread();
18 
19         // 釋放當前執行緒的鎖
20         lock.compareAndSet(t, null);
21     }
22 }

4、自適應自旋鎖

  在 JDK1.6 引入了自適應自旋。自旋時間不再固定,由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定。
  如果虛擬機器認為這次自旋很有可能成功,那就會持續較多的時間;如果自旋很少成功,那以後可能就直接省略掉自旋過程,避免浪費處理器資源。

六、鎖升級(無鎖|偏向鎖|輕量級鎖|重量級鎖)

  見《深入理解Synchronized》

相關文章