聊聊並發(七)——鎖

一、樂觀鎖和悲觀鎖

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》