Lock、Synchronized鎖區別解析

萌新J發表於2020-10-17

  上篇博文在講解 ConcurrentHashMap 時說到 1.7 中 put 方法實現同步的方式是使用繼承了 ReentrantLock 類的 segment 內部類呼叫 lock 方法實現的,而在 1.8 中是使用 synchronied 鎖住要新增資料對應陣列的第一個值實現的,關於這兩種鎖的區別時留了一個坑,現在來補下。眾所周知,在多執行緒下,對共享資料的操作需要格外小心,因為多執行緒下的各個執行緒執行的順序是無法預料的,所以對一個共享資料的操作可能會產生不同的結果,這時我們就需要讓執行緒對共享資料地操作按我們預期的方式去執行,得到預期的結果,實現這一方法就是使用鎖來限制,當然也可以直接使用同步容器,就比如 ConcurrentHashMap、Vector等,但是這些容器同步的實現還是靠 lock 或 synchronized 鎖。

 

監視器(monitor)

  在瞭解這兩種鎖之前,先要知道一個概念,"監視器"。

  監視器是作業系統實現同步的概念,一個監視器往往一個物件引用相關聯,當一個監視器開始監視某一段程式碼時,其他的執行緒就需要擁有這個監視器對應的物件,監視器確認後才能讓這個執行緒放行,繼續執行後面的程式碼。可以說  java 中的 synchronized、Lock 鎖這些就是監視器,是 "監視器" 這個概念的實現。

 

synchronized

  synchronized 是 java 的關鍵字,它可以修飾方法,程式碼塊,下面先簡單說一下它的用法。

  用法

  1、修飾例項方法  

 1   public synchronized void test() {
 2         if(sum<0) {
 3             n=false;
 4             return ;
 5         }
 6         try {
 7             Thread.sleep(200);
 8         } catch (InterruptedException e) {
 9             // TODO 自動生成的 catch 塊
10             e.printStackTrace();
11         }
12         System.out.println(Thread.currentThread().getName()+sum--);
13     }

  這種方式鎖住的是當前這個類的物件,如果兩個執行緒建立了不同的物件,那麼這個方法是鎖不住的,只有這兩個執行緒擁有同一個物件,然後拿這個物件作為鑰匙搶奪CPU然後進入方法執行。

 1 public class safe1{
 2     public static void main(String[] args) {
 3         QQ q=new QQ(); 
 4         new Thread(q,"張三").start();    // 如果這裡的 q 換成了 new QQ(),那麼就不能實現同步作用了
 5         new Thread(q,"李四").start();
 6         new Thread(q,"王五").start();
 7 
 8     }    
 9 
10 }
11 class QQ implements Runnable{
12     private int sum=100;
13     private boolean n=true;
14     
15     public synchronized void test() {
16         if(sum<0) {
17             n=false;
18             return ;
19         }
20         try {
21             Thread.sleep(200);
22         } catch (InterruptedException e) {
23             // TODO 自動生成的 catch 塊
24             e.printStackTrace();
25         }
26         System.out.println(Thread.currentThread().getName()+sum--);
27     }
28     public void run() {
29         while(n) {
30             test();
31         }
32         
33     }
34 
35 }

執行結果:

 

 上面這個例子是模擬搶票功能,可以看到三個執行緒在擁有同一個類物件時會實現同步,那麼如果把 “張三” 執行緒物件換成新 new 的物件,結果會怎樣呢?

 可以看到關於李四的票就會出現票數混亂,資料不能同步。

 

  2、修飾類方法(靜態方法)

  還是拿上面購票的例子來講解,如果 synchronized 修飾靜態方法,那麼鎖住的就是當前類,也就是 class 資訊,因為 class 資訊是在當前類載入時就被載入到方法區的,不同的物件都會擁有同一個該物件的類資訊,所以在多執行緒下即使是不同物件,最後的結果也能實現同步

 1 public class safe1{
 2     public static void main(String[] args) {
 3         new Thread(new QQ(),"張三").start();    
 4         new Thread(new QQ(),"李四").start();
 5         new Thread(new QQ(),"王五").start();
 6 
 7     }    
 8 
 9 }
10 class QQ implements Runnable{
11     private static int sum=100;
12     private static boolean n=true;
13     
14     public synchronized static void test() {
15         if(sum<0) {
16             n=false;
17             return ;
18         }
19         try {
20             Thread.sleep(200);
21         } catch (InterruptedException e) {
22             // TODO 自動生成的 catch 塊
23             e.printStackTrace();
24         }
25         System.out.println(Thread.currentThread().getName()+sum--);
26     }
27     public void run() {
28         while(n) {
29             test();
30         }
31         
32     }
33 
34 }

結果:

 

  3、修飾程式碼塊

  這個其實和前兩種方式差不多,只不過它修飾的變成了某一段程式碼塊,而前面兩種修飾的是整個方法的程式碼塊,並且修飾程式碼塊可以自定義 “鑰匙” ,這樣使得實現更加靈活,所以一般是推薦使用 synchronized 修飾程式碼塊實現執行緒同步的。同樣還是以上面購票為例

 1 public class safe1{
 2     public static void main(String[] args) {
 3         new Thread(new QQ(),"張三").start();    
 4         new Thread(new QQ(),"李四").start();
 5         new Thread(new QQ(),"王五").start();
 6 
 7     }    
 8 
 9 }
10 class QQ implements Runnable{
11     private static int sum=100;
12     private static boolean n=true;
13     
14     public void test() {
15         synchronized (QQ.class) {
16             if(sum<0) {
17                 n=false;
18                 return ;
19             }
20             try {
21                 Thread.sleep(200);
22             } catch (InterruptedException e) {
23                 // TODO 自動生成的 catch 塊
24                 e.printStackTrace();
25             }
26             System.out.println(Thread.currentThread().getName()+sum--);
27         }
28     }
29     public void run() {
30         while(n) {
31             test();
32         }
33         
34     }
35 
36 }

可以看到這次是在例項方法裡,如果修飾在方法上鎖住的就是當前類物件,不同執行緒必須擁有同一個物件才能實現同步,而在這個例子裡 synchronized 鎖住的是 .class 類資訊,所以最後還是能實現同步

 

  原理

  因為 synchronized 是關鍵字,沒有具體的類實現,所以我們只能在指令集上檢視,先上程式碼

    public void aa(){
        synchronized (this){
            int gg = 4;
        }
    }

    public void bb(){
        int gg = 4;
    }

在使用 jclass Bytecode viewer 工具將編譯後的 class 檔案轉成視覺化的指令集後,可以看到指令集如下:

  aa 方法:

  

   bb 方法:

  

   可以看到在 aa 方法中多了一些指令,其中比較重要的就是 "monitorenter"、"monitorexit"。這兩個指令對應的著 "解鎖" 和 "加鎖" ,也就是執行緒 "獲取到鎖" 以及程式碼執行完成後的 "釋放鎖"。"monitor" 就對應著文章開頭說的監視器,因為 synchronized 就是 java 中的 "監視器"。指令中有一次 "monitorenter" 代表執行緒得到 CPU 排程(也叫做執行緒得到了鎖),但是指令裡卻有兩次 "monitorexit",這是為了防止執行緒發生異常沒有執行 第一次的 "monitorexit",而導致其他執行緒永遠無法得到執行緒。 

 

  Synchronized 鎖升級機制

  在 JDK 早期的版本,synchronized 鎖的效率是非常低的,它的效率遠低於 lock 鎖,但是 sychronized 畢竟是 java 的關鍵詞,它不應該就此淘汰。所以在 JDK1.6 中對它進行優化,其實優化內容不僅僅是與 synchronized 有關的,還有 "自適應自旋鎖"、"鎖消除"、"鎖粗化"  等。這些優化和各種鎖以及多執行緒的其他知識後面會開單獨的一篇多執行緒的專欄來說。這裡先簡單的提一下,關於 synchronized 的優化就是它的升級機制。synchronized 也因為這個優化效率變得能和 Lock 鎖效率不相上下。

 

  1.6 之前的 synchronized 都是 "重量級鎖",什麼是重量級鎖呢?就是一個執行緒在獲取到 CPU 排程後,開始執行 synchronized 修飾的程式碼塊,這時其他執行到這裡的執行緒必須進行一次 "上下文切換"(下面有解釋)(其實在進行上下文切換前會先嚐試獲取鎖資源,失敗才會進行"上下文切換",這是非公平鎖的特性,下面 Lock 部分有講解,這裡比較的是 synchronized 效率問題,所以忽略一開始就搶奪到鎖資源的情況)和 "加鎖 "、"解鎖" 操作。這就是 "重量級鎖",這樣的鎖有嚴重的弊端。"上下文切換" 和 "加鎖"、 "解鎖" 這些動作雖然在單執行緒下消耗的時間並不算多,但是在一些高併發場景,例如百萬、千萬併發的場景,那麼這些動作消耗的總時間就比較大了;另外一種情況就是某段程式碼可能發生多個執行緒搶佔執行的情況,但是實際並沒有發生這種情況,都是一個執行緒執行完後下一個執行緒才執行到這段程式碼,這樣 "加鎖"、解鎖" 消耗的時間就浪費了。那麼有什麼方法去解決這個問題呢,這就是鎖升級機制帶來的好處。

上下文切換:執行緒之間的切換需要前一個執行緒先儲存當前的狀態,然後進入 "睡眠" 狀態,然後下一個執行緒 "啟動",執行,等到下一次前一個執行緒獲取到 CPU 排程時,再去讀取上次儲存的狀態,然後 "啟動"。我們把一個執行緒從儲存當前狀態到下一次"啟動"完成稱作這個執行緒的一次 "上下文切換"。

 

  synchronized 鎖升級機制是從 偏向鎖->輕量級鎖->重量級鎖 ,這個過程是不可逆的。

  在具體說這三種鎖時,先要了解物件頭的 Mark Word 部分,我們都知道物件上儲存著這個物件的一切資訊,包括它的地址、內部方法、屬性等資訊,前面說過監視器,就是一個鎖對應著一個物件,所以在物件上也儲存著這個物件所關聯鎖的資訊。關於鎖的資訊就儲存在物件物件頭的 Mark Word 部分上。下面是 Mark Word 結構示意圖:

下面說得偏向鎖、輕量級鎖、重量級鎖都會用到這上面的欄位。 

  1、偏向鎖

  

  首先是偏向鎖,偏向鎖是指一段程式碼同一時間內只有一個執行緒執行(這是在開啟了重偏向,如果沒有開啟重偏向則是一段程式碼一直只有一個執行緒執行)。當不滿足條件時就會升級成輕量級鎖。偏向鎖的執行邏輯是:

1、判斷 物件頭的 Mark Word 部分的鎖標誌位,01表示為偏向鎖,00輕量級鎖,10重量級鎖

2、判斷是否偏向鎖

  1、0,升級為輕量級鎖,然後執行相關策略

  2、1,檢查執行緒ID位是否是當前執行緒ID。

    1、是,獲得偏向鎖,執行程式碼

    2、否,嘗試進行CAS寫入當前執行緒ID

      1、成功。獲得鎖,執行

      2、失敗。說明已經存線上程ID了,會在安全的時間點暫停當前持有該偏向鎖的執行緒,然後判斷該執行緒是否存活

        1、存活,判斷該執行緒是否正在執行鎖住的程式碼

          1、正在執行,升級為輕量級鎖,然後執行輕量級鎖的相關策略(為該執行緒的棧中開啟一片區域來儲存複製的 mark work 記作 lock record,然後將鎖對應的物件物件頭的 mark word 部分的指標指向該執行緒,然後喚醒該執行緒繼續執行,在此期間當前執行緒也會在棧中拷貝一份 mark word然後使用自旋鎖+CAS樂觀鎖嘗試將該物件的 mark word 指標指向當前的 lock record,執行完輕量級鎖後 mark word 指標會刪除,以便後面的執行緒重新指向)

          2、沒有執行。檢查是否開啟重偏向。

            1、開啟了,先設定為匿名偏向狀態,然後將 mark word 的 threadId 寫入當前的執行緒ID位置,然後再喚醒執行緒,繼續執行

            2、沒有開啟,先撤銷偏向鎖,將 mark word 設定為無鎖狀態,然後升級輕量級鎖,執行輕量級鎖的執行策略

        2、沒有存活,檢查是否開啟重偏向。

 

從上面的執行策略來看,偏向鎖下是沒有加鎖、釋放鎖的操作的,這樣就加快了對某段一段時間內只有一個執行緒執行的程式碼的執行效率。上面還提到自旋鎖,樂觀鎖。這裡是準備後面再開一篇多執行緒的部落格專門來說這些,現在先簡單說一下。

自旋鎖:由於執行緒切換需要進行 "上下文切換",這個過程一次兩次可能不算耗時,但是在多執行緒下,特別是在高併發場景下大量執行緒頻繁地進行執行緒切換,就會出現大量的 "上下文切換",這中間消耗的時間是非常長的,所以對於這部分程式碼就使用 "自旋鎖",它的特點是不會儲存當前執行緒狀態,也不會進入 "睡眠狀態",而是一直嘗試獲取 CPU 排程,保持一種 "執行" 狀態,這樣就省去了 "上下文切換" 的時間,當然,這隻適用於多核 CPU ,單核 CPU 是不能發揮 "自旋鎖" 的作用的,因為它在一直嘗試,這個嘗試的過程也會佔用 CPU 。

樂觀鎖先儲存一個參考資料,然後修改當前執行緒空間的變數,然後準備更新到主記憶體中去,在更新之前檢查主記憶體對應的參考資料是否與之前儲存的參考資料一致,如果一致更新到主記憶體,如果不一樣那麼此次更新作廢。

 

  2、輕量級鎖

 

  輕量級鎖適用於執行緒數量少且執行時間短的程式碼塊。線上程還未得到CPU排程時,首先會在該執行緒的棧中開啟一塊區域作為lock record,然後將物件頭的 Mark Word 部分拷貝到 lock record 位置,然後嘗試將物件物件頭 Mark Word 輕量級鎖部分的指向棧的指標指向自己執行緒的lock record,如果成功就表明該執行緒得到了鎖,CPU就會排程。詳細的執行過程是:

  1.如果這個物件鎖是剛剛升級到輕量級鎖且鎖對應物件的mark word的偏向鎖部分儲存的 threadId 對應的執行緒沒有執行完當前對應的程式碼,那麼系統就會先將CPU交給 threadId 對應的執行緒,讓他先執行完。過程就是先在該執行緒的棧中開啟一塊區域作為lock record,然後將mark word拷貝到 lock record,再將輕量級鎖部分的指標指向 lock record。隨後開始執行鎖修飾的程式碼塊,執行完畢後會進行兩次檢查:1.物件頭的Mark Word中鎖記錄指標是否還是指向當前執行緒的lock record部分  2.lock record是否還與物件頭的Mark Word一致。如果一致,就釋放鎖資源。如果不一致就將鎖升級為重量級鎖,然後釋放。

  2.如果是普通的執行緒,那麼首先還是在當前執行緒的棧中開啟一塊區域作為lock record,然後將物件頭的 Mark Word 部分拷貝到 lock record 位置,然後嘗試將物件物件頭 Mark Word 輕量級鎖部分的指向棧的指標指向自己執行緒的lock record,

    1.如果成功,就繼續執行後面程式碼,

    2.如果失敗就以自旋鎖方式繼續嘗試,

      1.如果一定次數還是沒有獲取到鎖,那麼就將鎖膨脹為重量級鎖。

      2.如果成功執行鎖修飾的程式碼,執行完會再進行兩個檢查,如果符合就釋放鎖。不符合就膨脹成重量級鎖,然後再釋放。

 

  3、重量級鎖

  重量級鎖前面也說過了,就是一個執行緒在執行時,其他執行緒就先儲存當前執行緒狀態,然後進入 "休眠" 狀態,乖乖等待CPU分配,得到CPU後才會讀取上一次儲存的狀態,然後繼續執行。它的執行邏輯還是先判斷Mark Word部分的鎖標誌位,是10就說明是重量級鎖,然後先來的嘗試獲取,得到CPU,繼續執行,後面的執行緒就需要等待進行一次上下文切換。

 

  總結:

  正是因為 synchronized 鎖升級機制的存在,使得 synchronized 的效率不再那麼低。

 

   等待-通知模型

   在一個執行緒執行 synchronized 修飾的程式碼塊時,其他執行緒並不是必須等到該執行緒執行完才可能得到 CPU 排程,對於某些業務場景,需要我們在一段程式碼中進行執行緒地來回切換。這就需要說到 "等待-通知" 模型了,在說這個模型前,要先了解 Object 的 wait() 方法

1 public final void wait() throws InterruptedException {
2         wait(0);
3 }
4 
5 
6 
7 
8 
9 public final native void wait(long timeout) throws InterruptedException;

可以看到 wait(long timeout) 是使用 native 修飾的,是一個本地方法,用 C、C++實現的,這個方法作用就是讓擁有這個物件的執行緒釋放掉這個物件,進入 "休眠" ,並讓出 CPU,讓其他執行緒去得到物件資源執行。與其對應的就是 notify() 方法,它也是 Object 類的方法,這個方法會隨機讓該物件對應鎖的一個 "休眠" 的執行緒 "甦醒",然後參與CPU的競爭中。還有一個 notifyAll() 是讓物件對應鎖的所有 "休眠" 的執行緒 "甦醒"。下面來看一個例子

 1 public class CoTest01 {
 2     public static void main(String[] args) {
 3         SynContainer container = new SynContainer();
 4         new Productor(container).start();
 5         new Consumer(container).start();
 6     }
 7 }
 8 //生產者
 9 class Productor extends Thread{
10     SynContainer container  ;    
11     public Productor(SynContainer container) {
12         this.container = container;
13     }
14 
15     public void run() {
16         //生產
17 //        synchronized (container) {
18             for(int i=0;i<100;i++) {
19                 container.push(new Steamedbun(i));
20             }
21 //        }
22     }
23 }
24 //消費者
25 class Consumer extends Thread{
26     SynContainer container  ;    
27     public Consumer(SynContainer container) {
28         this.container = container;
29     }
30     public void run() {
31 //        synchronized (container) {
32             //消費
33             for(int i=0;i<100;i++) {
34                 container.pop();
35             }
36 //        }
37     }
38 }
39 //緩衝區
40 class SynContainer{
41     List<Steamedbun> list=new ArrayList<>(); //儲存容器
42     //儲存 生產
43     public synchronized void push(Steamedbun bun) {
44         //何時能生產  容器存在空間
45         //不能生產 只有等待
46         if(list.size() == 10) {
47             try {
48                 this.wait(); //執行緒阻塞  消費者通知生產解除
49             } catch (InterruptedException e) {
50                 System.out.println("push 異常");
51             }
52         }
53         //存在空間 可以生產
54         list.add(bun);
55         //存在資料了,可以通知消費了
56         this.notifyAll();
57         System.out.println("生產-->"+list.size()+"個饅頭");
58     }
59     //獲取 消費
60     public synchronized void pop() {
61         //何時消費 容器中是否存在資料
62         //沒有資料 只有等待
63         if(list.size() == 0) {
64             try {
65                 this.wait(); //執行緒阻塞  生產者通知消費解除
66             } catch (InterruptedException e) {
67             }
68         }
69         //存在資料可以消費
70         list.remove(list.size()-1);
71         this.notifyAll(); //存在空間了,可以喚醒對方生產了
72         System.out.println("消費第" + list.size() + "個饅頭");
73     }
74 }
75 //饅頭
76 class Steamedbun{
77     int id;
78     public Steamedbun(int id) {
79         this.id = id;
80     }
81     
82 }

上面這個例子就是典型的 "等待-通知" 模型。通過 wait() 和 notifyAll() 來控制執行緒之間的切換。

執行結果:

 

 

 Lock

   lock 是 java 核心類庫中的一個介面,它是 jdk 維護的實現同步的一個介面,常用的實現類是 RenntrantLock、ReadWriteLock 等。相比於 synchronized,它更靈活,效率也更高(jdk6之前)。下面就先以 ReeentrantLock 為例,來說一下Lock 介面中常用的構造器和實現方法。在說構造器前先要了解什麼是公平鎖,非公平鎖。它不能修飾方法,只能修飾程式碼塊。

 

  公平鎖

  公平鎖指的是一個執行緒在執行到一段鎖包裹的程式碼前,發現已經有其他執行緒到了,並且已經排成了一個 "等待隊伍" (這個屬於AQS定義的執行緒執行規則,也會在後續的多執行緒部落格中說到),排隊等待CPU排程,那麼公平鎖的策略是直接加入這個 "等待隊伍" 的最後面,保證鎖資源的公平性分配。

 

  非公平鎖  

  非公平鎖指的是在遇到 "等待佇列" 時,會先嚐試獲取鎖資源,如果獲取到直接 "插隊" 執行,如果沒有獲取到就乖乖到最後面 "排隊"。雖然這種鎖看起來不 "文明",但是它的總體效率是比 "公平鎖" 要高的,如果在 "隊頭" 的執行緒發生異常停止執行,那麼後面的執行緒就需要一直等待影響效率。但是這樣也會導致優先順序低的執行緒在和高優先順序的執行緒競爭時一直沒有獲取到CPU,從而一直無法執行造成 "活鎖"。所以總結下來就是非公平鎖會使系統整體的效率提高但是可能會導致 "活鎖" 的情況發生。 上面的 synchronized 就是非公平鎖。

  構造器 

   /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

  可以看到, ReetrantLock 有兩個構造方法,這也是Lock 實現類通有的,我們重點看一下第二種構造器,因為第一種的實現程式碼只不過是第二種構造器中程式碼的一種。引數 fair 表示是否是公平鎖?如果是 true ,那麼建立的 ReetrantLock 物件就是公平鎖物件,如果 false 或者沒有指定引數那麼建立出來的物件都是非公平鎖物件。而 synchronized 只可能是非公平鎖。

 

  常用方法:

  下面就以ReentranLock 為例,說一下lock的常用實現方法。

  1、Lock()

    lock() 方法其實和 synchronized 的重量級鎖執行策略是一樣的,當然如果在物件建立時指定公平鎖,那麼會直接進入 “等待佇列” ,如果沒有指定或者是指定為非公平鎖那麼會先嚐試獲取鎖資源。然後沒有獲取到就會進行一次上下文切換。

   2、tryLock()

    嘗試獲取鎖資源,獲取到就直接執行後面的程式碼並返回 true,如果沒有獲取到直接退出不進入 "等待佇列" 並返回 false 。

  3、tryLock(long time,TimeUnit unit)

    嘗試在一段時間內獲取鎖資源,獲取到就執行後面的程式碼並返回 true , 否則退出返回 false 。可以通過 interrupt() 方法中斷阻塞狀態。

  4、lockInterruptibly()

    和 lock()一樣,只不過可以呼叫該執行緒的 interrupt() 方法去中斷,而 Lock() 方法不會被中斷,只能獲取到鎖資源的執行緒呼叫了 unlock方法才會中斷等待狀態。

 

 同步,等待通知模型實現

  同步實現:lock 的加鎖的物件就是 lock 本身的物件,所以我們只需要呼叫 lock()方法就可以實現加鎖操作,但是與 synchronized 不同的是 lock 鎖需要手動的去釋放,也就是呼叫 unlock() 方法去釋放當前物件的鎖,所以unlock()方法一般是在 finally 修飾的程式碼塊中,防止上面發生異常而沒有釋放鎖導致死鎖。

  等待-通知模式實現:lock 的 "等待-通知模式" 是通過 Condition 類實現的,呼叫 lock 物件的 newCondition() 方法去建立與之對應的 Condition 物件,然後呼叫 Condition 物件的 await 方法實現阻塞並釋放資源給其他執行緒,等到其他執行緒執行完再呼叫 Condition 的 signal() 方法(對應Object中的 notify()方法)去隨機喚醒一個阻塞的執行緒,而 signalAll()(對應notifyAll())則是喚醒 lock 物件對應的所有阻塞執行緒。 

下面就用一個例子來實現。

 1 public class lock2{
 2     
 3     static lock2 ll=new lock2();
 4     ReentrantLock lock=new ReentrantLock();
 5     Condition cc=lock.newCondition();
 6     
 7     public static void main(String[] args) {
 8         new Thread(new qq()).start();
 9         new Thread(new qq2()).start();
10     }
11     public static class qq implements Runnable{
12 
13         @Override
14         public void run() {
15             ll.aa();
16         }
17         
18     }
19     public static class qq2 implements Runnable{
20         
21         @Override
22         public void run() {
23             ll.bb();
24         }
25         
26     }
27     
28     public void aa() {
29         lock.lock();
30         try {
31             System.out.println("aa方法開始了"+Thread.currentThread().getName());
32             Thread.sleep(2000);
33             cc.await();
34             System.out.println("aa方法結束了"+Thread.currentThread().getName());
35         } catch (InterruptedException e) {
36             e.printStackTrace();
37         }finally {
38             lock.unlock();
39         }
40     }
41     public void bb() {
42         lock.lock();
43         try {
44             System.out.println("bb方法開始了"+Thread.currentThread().getName());
45             Thread.sleep(2000);
46             System.out.println("bb方法結束了"+Thread.currentThread().getName());
47             cc.signal();
48         } catch (InterruptedException e) {
49             e.printStackTrace();
50         }finally {
51             lock.unlock();
52         }
53     }
54     
55 }

 

結果:

 在 "aa方法開始了Thread-0" 輸出後,等待了兩秒後,通過 await 方法阻塞當前執行緒,然後把鎖資源讓給 "Thread-1",輸出 "bb方法開始了Thread-1" ,兩秒後再輸出 "bb方法結束了Thread-1",然後通過signal喚醒,因為這裡只有一個 "Thread-0" 執行緒阻塞所以直接喚醒 "Thread-0",最後輸出 "aa方法結束了Thread-0" 執行完畢。

 

  ReentrantReadWriteLock

  ReentrantReadWriteLock 是一種特殊的 Lock實現類,它除了可以實現上面提到的所有功能外,還能實現 "共享鎖" 和 "排他鎖" 。它的讀鎖就是 "共享鎖" ,寫鎖是 "排他鎖" 。

  

  共享鎖

    共享鎖就是不同執行緒可以同時執行,相當於沒有加鎖。那麼問題來了,既然多執行緒可以同時獲取共享鎖,那麼共享鎖的意義是什麼呢?答案就是為了和 "排他鎖" 互斥。下面先看 ReentrantReadWriteLock 共享鎖的例子。

 1 public class Test {
 2     private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 3     
 4     public static void main(String[] args)  {
 5         final Test test = new Test();
 6         new Thread(){
 7             public void run() {
 8                 test.get(Thread.currentThread());
 9             };
10         }.start();
11         new Thread(){
12             public void run() {
13                 test.get(Thread.currentThread());
14             };
15         }.start();
16     }  
17     public void get(Thread thread) {
18         rwl.readLock().lock();
19         try {
20             long start = System.currentTimeMillis();
21             while(System.currentTimeMillis() - start <= 1) {
22                 System.out.println(thread.getName()+"正在進行讀操作");
23             }
24             System.out.println(thread.getName()+"讀操作完畢");
25         } finally {
26             rwl.readLock().unlock();
27         }
28     }
29 }

結果:

 可以看到,在呼叫 readLock的lock()方法後,兩個執行緒依然能交叉執行,這就是共享鎖的特點

 排它鎖

  排它鎖就是我們常見的鎖,同一時間鎖資源只能被一個執行緒所佔用,它和 "共享鎖" 是互斥關係。下面還是以上面的程式碼改成 "寫鎖"試試。

 1 public class Test {
 2     private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 3     
 4     public static void main(String[] args)  {
 5         final Test test = new Test();
 6         new Thread(){
 7             public void run() {
 8                 test.get(Thread.currentThread());
 9             };
10         }.start();
11         new Thread(){
12             public void run() {
13                 test.get(Thread.currentThread());
14             };
15         }.start();
16     }  
17     public void get(Thread thread) {
18         rwl.writeLock().lock();
19         try {
20             long start = System.currentTimeMillis();
21             while(System.currentTimeMillis() - start <= 1) {
22                 System.out.println(thread.getName()+"正在進行寫操作");
23             }
24             System.out.println(thread.getName()+"寫操作完畢");
25         } finally {
26             rwl.writeLock().unlock();
27         }
28     }
29 }

結果:

可以看到,兩個執行緒是互斥關係。

 

  為了嚴謹,再比較一下 "讀鎖" 與 "寫鎖" 的互斥關係

 1 public class Test {
 2     private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 3     
 4     public static void main(String[] args)  {
 5         final Test test = new Test();
 6         new Thread(){
 7             public void run() {
 8                 test.getWrite(Thread.currentThread());
 9             };
10         }.start();
11         new Thread(){
12             public void run() {
13                 test.getRead(Thread.currentThread());
14             };
15         }.start();
16     }  
17     public void getWrite(Thread thread) {
18         rwl.writeLock().lock();
19         try {
20             long start = System.currentTimeMillis();
21             while(System.currentTimeMillis() - start <= 1) {
22                 System.out.println(thread.getName()+"正在進行寫操作");
23             }
24             System.out.println(thread.getName()+"寫操作完畢");
25         } finally {
26             rwl.writeLock().unlock();
27         }
28     }
29     public void getRead(Thread thread) {
30         rwl.readLock().lock();
31         try {
32             long start = System.currentTimeMillis();
33             while(System.currentTimeMillis() - start <= 1) {
34                 System.out.println(thread.getName()+"正在進行讀操作");
35             }
36             System.out.println(thread.getName()+"讀操作完畢");
37         } finally {
38             rwl.readLock().unlock();
39         }
40     }
41 }

 結果:   

 

總結  

  Lock鎖synchronized鎖區別

  1. Lock介面實現的類鎖是核心類庫中的程式碼,是Java編寫的;synchronized是關鍵字,屬於JVM,也就是Java原生的,使用其他語言實現。
  2. Lock實現類鎖有更多方法,比如可以選擇是公平鎖還是非公平鎖;一段時間獲取不到資源可以退出等待佇列;以及共享鎖排它鎖;而後者功能就比較單一了。
  3. synchronized 可以修飾靜態方法、例項方法、程式碼塊;Lock 實現的鎖只能修飾程式碼塊
  4. synchronized不需要釋放鎖,Lock鎖需要手動釋放。

  相同點:

  1. 都可以修飾程式碼塊
  2. 都是可重入鎖
  3. 效率差不多(jdk1.6優化以後)

 

 

相關文章