1. 程式人生 > >[轉]Java 併發:Lock 框架詳解

[轉]Java 併發:Lock 框架詳解

摘要:

  我們已經知道,synchronized 是java的關鍵字,是Java的內建特性,在JVM層面實現了對臨界資源的同步互斥訪問,但 synchronized 粒度有些大,在處理實際問題時存在諸多侷限性,比如響應中斷等。Lock 提供了比 synchronized更廣泛的鎖操作,它能以更優雅的方式處理執行緒同步問題。本文以synchronized與Lock的對比為切入點,對Java中的Lock框架的枝幹部分進行了詳細介紹,最後給出了鎖的一些相關概念。

一. synchronized 的侷限性 與 Lock 的優點

  回顧文章《Java 併發:內建鎖 Synchronized》

,如果一個程式碼塊被synchronized關鍵字修飾,當一個執行緒獲取了對應的鎖,並執行該程式碼塊時,其他執行緒便只能一直等待直至佔有鎖的執行緒釋放鎖。事實上,佔有鎖的執行緒釋放鎖一般會是以下三種情況之一:

  • 佔有鎖的執行緒執行完了該程式碼塊,然後釋放對鎖的佔有;
  • 佔有鎖執行緒執行發生異常,此時JVM會讓執行緒自動釋放鎖;
  • 佔有鎖執行緒進入 WAITING 狀態從而釋放鎖,例如在該執行緒中呼叫wait()方法等。

synchronized 是Java語言的內建特性,可以輕鬆實現對臨界資源的同步互斥訪問。那麼,為什麼還會出現Lock呢?試考慮以下三種情況:

Case 1 :
  在使用synchronized關鍵字的情形下,假如佔有鎖的執行緒由於要等待IO或者其他原因(比如呼叫sleep方法)被阻塞了,但是又沒有釋放鎖,那麼其他執行緒就只能一直等待,別無他法。這會極大影響程式執行效率。因此,就需要有一種機制可以不讓等待的執行緒一直無期限地等待下去(比如只等待一定的時間 (解決方案:tryLock(long time, TimeUnit unit))
或者 能夠響應中斷 (解決方案:lockInterruptibly())),這種情況可以通過 Lock 解決。
Case 2 :
  我們知道,當多個執行緒讀寫檔案時,讀操作和寫操作會發生衝突現象,寫操作和寫操作也會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。但是如果採用synchronized關鍵字實現同步的話,就會導致一個問題,即當多個執行緒都只是進行讀操作時,也只有一個執行緒在可以進行讀操作,其他執行緒只能等待鎖的釋放而無法進行讀操作。因此,需要一種機制來使得當多個執行緒都只是進行讀操作時,執行緒之間不會發生衝突。同樣地,Lock也可以解決這種情況 (解決方案:ReentrantReadWriteLock)
Case 3 :
  我們可以通過Lock得知執行緒有沒有成功獲取到鎖 (解決方案:ReentrantLock) ,但這個是synchronized無法辦到的。

  上面提到的三種情形,我們都可以通過Lock來解決,但 synchronized 關鍵字卻無能為力。事實上,Lock 是 java.util.concurrent.locks包 下的介面,Lock 實現提供了比 synchronized 關鍵字 更靈活、更廣泛、粒度更細 的鎖操作,它能以更優雅的方式處理執行緒同步問題。也就是說,Lock提供了比synchronized更多的功能。但是要注意以下幾點:

  1) synchronized是Java的關鍵字,因此是Java的內建特性,是基於JVM層面實現的,其經過編譯之後,會在同步塊的前後分別形成 monitorenter 和 monitorexit 兩個位元組碼指令;而Lock是一個Java介面,是基於JDK層面實現的,通過這個介面可以實現同步訪問;

  2) 採用synchronized方式不需要使用者去手動釋放鎖,當synchronized方法或者synchronized程式碼塊執行完之後,系統會自動讓執行緒釋放對鎖的佔用;而 Lock則必須要使用者去手動釋放鎖 (發生異常時,不會自動釋放鎖)如果沒有主動釋放鎖,就有可能導致死鎖現象。

  這是很好理解的。Synchronized方式是Java原生支援的,開發人員在使用它來解決併發問題時,一定會方便很多,在這裡,開發人員就不需要手動獲取鎖和釋放鎖,這些操作均有Java自身自動完成;而Lock方式是JDK層面的提供給開發人員的介面,因此開發人員在使用它來解決併發問題時,需要手動獲取鎖和釋放鎖。

二. java.util.concurrent.locks包下常用的類與介面

  以下是 java.util.concurrent.locks包下主要常用的類與介面的關係:

           

1、Lock

  通過檢視Lock的原始碼可知,Lock 是一個介面:

public interfaceLock {
    void lock();
    void lockInterruptibly() throws InterruptedException;  // 可以響應中斷
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  // 可以響應中斷
    void unlock();
    Condition newCondition();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

  下面來逐個分析Lock介面中每個方法。lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用來獲取鎖的。unLock()方法是用來釋放鎖的。newCondition() 返回 繫結到此 Lock 的新的 Condition 例項 ,用於執行緒間的協作,詳細內容見文章《Java 併發:執行緒間通訊與協作》

1). lock()

  在Lock中聲明瞭四個方法來獲取鎖,那麼這四個方法有何區別呢?首先,lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他執行緒獲取,則進行等待。在前面已經講到,如果採用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此,一般來說,使用Lock必須在try…catch…塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){

}finally{
    lock.unlock();   //釋放鎖
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2). tryLock() & tryLock(long time, TimeUnit unit)

  tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true;如果獲取失敗(即鎖已被其他執行緒獲取),則返回false,也就是說,這個方法無論如何都會立即返回(在拿不到鎖時不會一直在那等待)。

  tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false,同時可以響應中斷。如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。

  一般情況下,通過tryLock來獲取鎖時是這樣使用的:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){

     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能獲取鎖,則直接做其他事情
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

3). lockInterruptibly()

  lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果執行緒 正在等待獲取鎖,則這個執行緒能夠響應中斷,即中斷執行緒的等待狀態。例如,當兩個執行緒同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時執行緒A獲取到了鎖,而執行緒B只有在等待,那麼對執行緒B呼叫threadB.interrupt()方法能夠中斷執行緒B的等待過程。

  由於lockInterruptibly()的宣告中丟擲了異常,所以lock.lockInterruptibly()必須放在try塊中或者在呼叫lockInterruptibly()的方法外宣告丟擲 InterruptedException,但推薦使用後者,原因稍後闡述。因此,lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  注意,當一個執行緒獲取了鎖之後,是不會被interrupt()方法中斷的。因為interrupt()方法只能中斷阻塞過程中的執行緒而不能中斷正在執行過程中的執行緒。因此,當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,那麼只有進行等待的情況下,才可以響應中斷的。與 synchronized 相比,當一個執行緒處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。

  最佳實踐 (Best Practice):在使用Lock時,無論以哪種方式獲取鎖,習慣上最好一律將獲取鎖的程式碼放到 try…catch…,因為我們一般將鎖的unlock操作放到finally子句中,如果執行緒沒有獲取到鎖,在執行finally子句時,就會執行unlock操作,從而丟擲 IllegalMonitorStateException,因為該執行緒並未獲得到鎖卻執行了解鎖操作。

2、ReentrantLock

  ReentrantLock,即可重入鎖。ReentrantLock是唯一實現了Lock介面的類,並且ReentrantLock提供了更多的方法。下面通過一些例項學習如何使用 ReentrantLock。

例 1 : Lock 的正確使用

public classTest {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();

    public static void main(String[] args) {
        final Test test = new Test();

        new Thread("A") {
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();

        new Thread("B") {
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }

    public void insert(Thread thread) {
        Lock lock = new ReentrantLock();  // 注意這個地方:lock被宣告為區域性變數
        lock.lock();
        try {
            System.out.println("執行緒" + thread.getName() + "得到了鎖...");
            for (int i = 0; i < 5; i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {

        } finally {
            System.out.println("執行緒" + thread.getName() + "釋放了鎖...");
            lock.unlock();
        }
    }
}/* Output: 
        執行緒A得到了鎖...
        執行緒B得到了鎖...
        執行緒A釋放了鎖...
        執行緒B釋放了鎖...
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

  結果或許讓人覺得詫異。第二個執行緒怎麼會在第一個執行緒釋放鎖之前得到了鎖?原因在於,在insert方法中的lock變數是區域性變數,每個執行緒執行該方法時都會儲存一個副本,那麼每個執行緒執行到lock.lock()處獲取的是不同的鎖,所以就不會對臨界資源形成同步互斥訪問。因此,我們只需要將lock宣告為成員變數即可,如下所示。

public classTest {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();  // 注意這個地方:lock被宣告為成員變數
    ...
}/* Output: 
        執行緒A得到了鎖...
        執行緒A釋放了鎖...
        執行緒B得到了鎖...
        執行緒B釋放了鎖...
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

例 2 : tryLock() & tryLock(long time, TimeUnit unit)

public classTest {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock(); // 注意這個地方:lock 被宣告為成員變數

    public static void main(String[] args) {
        final Test test = new Test();

        new Thread("A") {
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();

        new Thread("B") {
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }

    public void insert(Thread thread) {
        if (lock.tryLock()) {     // 使用 tryLock()
            try {
                System.out.println("執行緒" + thread.getName() + "得到了鎖...");
                for (int i = 0; i < 5; i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {

            } finally {
                System.out.println("執行緒" + thread.getName() + "釋放了鎖...");
                lock.unlock();
            }
        } else {
            System.out.println("執行緒" + thread.getName() + "獲取鎖失敗...");
        }
    }
}/* Output: 
        執行緒A得到了鎖...
        執行緒B獲取鎖失敗...
        執行緒A釋放了鎖...
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

  與 tryLock() 不同的是,tryLock(long time, TimeUnit unit) 能夠響應中斷,即支援對獲取鎖的中斷,但嘗試獲取一個內部鎖的操作(進入一個 synchronized 塊)是不能被中斷的。如下所示:

public classTest {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test,"A");
        MyThread thread2 = new MyThread(test,"B");
        thread1.start();
        thread2.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  

    public void insert(Thread thread) throws InterruptedException{
        if(lock.tryLock(4, TimeUnit.SECONDS)){
            try {
                System.out.println("time=" + System.currentTimeMillis() + " ,執行緒 " + thread.getName()+"得到了鎖...");
                long now = System.currentTimeMillis();
                while (System.currentTimeMillis() - now < 5000) {
                    // 為了避免Thread.sleep()而需要捕獲InterruptedException而帶來的理解上的困惑,
                    // 此處用這種方法空轉5秒
                }
            }finally{
                lock.unlock();
            }
        }else {
            System.out.println("執行緒 " + thread.getName()+"放棄了對鎖的獲取...");
        }
    }
}

class MyThread extends Thread {
    private Test test = null;

    public MyThread(Test test,String name) {
        super(name);
        this.test = test;
    }

    @Override
    public void run() {
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println("time=" + System.currentTimeMillis() + " ,執行緒 " + Thread.currentThread().getName() + "被中斷...");
        }
    }
}/* Output: 
        time=1486693682559, 執行緒A 得到了鎖...
        time=1486693684560, 執行緒B 被中斷...(響應中斷,時間恰好間隔2s)
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

例 3 : 使用 lockInterruptibly() 響應中斷

public classTest {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test,"A");
        MyThread thread2 = new MyThread(test,"B");
        thread1.start();
        thread2.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  

    public void insert(Thread thread) throws InterruptedException{
        //注意,如果需要正確中斷等待鎖的執行緒,必須將獲取鎖放在外面,然後將 InterruptedException 丟擲
        lock.lockInterruptibly(); 
        try {  
            System.out.println("執行緒 " + thread.getName()+"得到了鎖...");
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {              // 耗時操作
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
                //插入資料
            }
        }finally {
            System.out.println(Thread.currentThread().getName()+"執行finally...");
            lock.unlock();
            System.out.println("執行緒 " + thread.getName()+"釋放了鎖");
        } 
        System.out.println("over");
    }
}

class MyThread extends Thread {
    private Test test = null;

    public MyThread(Test test,String name) {
        super(name);
        this.test = test;
    }

    @Override
    public void run() {
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println("執行緒 " + Thread.currentThread().getName() + "被中斷...");
        }
    }
}/* Output: 
        執行緒 A得到了鎖...
        執行緒 B被中斷...
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

  執行上述程式碼之後,發現 thread2 能夠被正確中斷,放棄對任務的執行。特別需要注意的是,如果需要正確中斷等待鎖的執行緒,必須將獲取鎖放在外面(try 語句塊外),然後將 InterruptedException 丟擲。如果不這樣做,像如下程式碼所示:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public classTest {
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        Test test = new Test();
        MyThread thread1 = new MyThread(test, "A");
        MyThread thread2 = new MyThread(test, "B");
        thread1.start();
        thread2.start();

        try {
            Thread.sleep(5000);
            System.out.println("執行緒" + Thread.currentThread().getName()
                    + " 睡醒了...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }

    public void insert(Thread thread) {

        try {
            // 注意,如果將獲取鎖放在try語句塊裡,則必定會執行finally語句塊中的解鎖操作。
            // 若執行緒在獲取鎖時被中斷,則再執行解鎖操作就會導致異常,因為該執行緒並未獲得到鎖。
            lock.lockInterruptibly();
            System.out.println("執行緒 " + thread.getName() + "得到了鎖...");
            long startTime = System.currentTimeMillis();
            for (;;) {
                if (System.currentTimeMillis() - startTime >= Integer.MAX_VALUE) // 耗時操作
                    break;
                // 插入資料
            }
        } catch (Exception e) {

        } finally {
            System.out.println(Thread.currentThread().getName()
                    + "執行finally...");
            lock.unlock();
            System.out.println("執行緒 " + thread.getName() + "釋放了鎖...");
        }
    }
}

class MyThread extends Thread {
    private Test test = null;

    public MyThread(Test test, String name) {
        super(name);
        this.test = test;
    }

    @Override
    public void run() {

        test.insert(Thread.currentThread());
        System.out.println("執行緒 " + Thread.currentThread().getName() + "被中斷...");
    }
}/* Output: 
        執行緒A 得到了鎖...
        執行緒main 睡醒了...
        B執行finally...
        Exception in thread "B" 
            java.lang.IllegalMonitorStateException
            at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(Unknown Source)
            at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(Unknown Source)
            at java.util.concurrent.locks.ReentrantLock.unlock(Unknown Source)
            at Test.insert(Test.java:39)
            at MyThread.run(Test.java:56)
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

  注意,上述程式碼就將鎖的獲取操作放在try語句塊裡,則必定會執行finally語句塊中的解鎖操作。在 準備獲取鎖的 執行緒B 被中斷後,再執行解鎖操作就會丟擲 IllegalMonitorStateException,因為該執行緒並未獲得到鎖卻執行了解鎖操作。

3、ReadWriteLock

  ReadWriteLock也是一個介面,在它裡面只定義了兩個方法:

public interfaceReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

  一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說,將對臨界資源的讀寫操作分成兩個鎖來分配給執行緒,從而使得多個執行緒可以同時進行讀操作。下面的 ReentrantReadWriteLock 實現了 ReadWriteLock 介面。

4、ReentrantReadWriteLock

  ReentrantReadWriteLock 實現了 ReadWriteLock 介面( 注意,ReentrantReadWriteLock 並沒有實現 Lock 介面 ),其包含兩個很重要的方法:readLock() 和 writeLock() 分別用來獲取讀鎖和寫鎖,並且這兩個鎖實現了Lock介面。下面通過幾個例子來看一下ReentrantReadWriteLock具體用法。假如有多個執行緒要同時進行讀操作的話,先看一下synchronized達到的效果:

public classTest {
    public static void main(String[] args)  {
        final Test test = new Test();

        new Thread("A"){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();

        new Thread("B"){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();

    }  

    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        System.out.println("執行緒"+ thread.getName()+"開始讀操作...");
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println("執行緒"+ thread.getName()+"正在進行讀操作...");
        }
        System.out.println("執行緒"+ thread.getName()+"讀操作完畢...");
    }
}/* Output: 
        執行緒A開始讀操作...
        執行緒A正在進行讀操作...
        ...
        執行緒A正在進行讀操作...
        執行緒A讀操作完畢...
        執行緒B開始讀操作...
        執行緒B正在進行讀操作...
        ...
        執行緒B正在進行讀操作...
        執行緒B讀操作完畢...
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

  這段程式的輸出結果會是,直到執行緒A執行完讀操作之後,才會列印執行緒B執行讀操作的資訊。而改成使用讀寫鎖的話:

public classTest {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final Test test = new Test();

        new Thread("A") {
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();

        new Thread("B") {
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
    }

    public void get(Thread thread) {
        rwl.readLock().lock(); // 在外面獲取鎖
        try {
            long start = System.currentTimeMillis();
            System.out.println("執行緒" + thread.getName() + "開始讀操作...");
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println("執行緒" + thread.getName() + "正在進行讀操作...");
            }
            System.out.println("執行緒" + thread.getName() + "讀操作完畢...");
        } finally {
            rwl.readLock().unlock();
        }
    }
}/* Output: 
        執行緒A開始讀操作...
        執行緒B開始讀操作...
        執行緒A正在進行讀操作...
        執行緒A正在進行讀操作...
        執行緒B正在進行讀操作...
        ...
        執行緒A讀操作完畢...
        執行緒B讀操作完畢...
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

  我們可以看到,執行緒A和執行緒B在同時進行讀操作,這樣就大大提升了讀操作的效率。不過要注意的是,如果有一個執行緒已經佔用了讀鎖,則此時其他執行緒如果要申請寫鎖,則申請寫鎖的執行緒會一直等待釋放讀鎖。如果有一個執行緒已經佔用了寫鎖,則此時其他執行緒如果申請寫鎖或者讀鎖,則申請的執行緒也會一直等待釋放寫鎖。

5、Lock 和 Synchronized 的選擇

總的來說,Lock 和 Synchronized 有以下幾點不同:

  (1). Lock是一個介面,是JDK層面的實現;而synchronized是Java中的關鍵字,是Java的內建特性,是JVM層面的實現;

  (2). synchronized 在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;

  (3). Lock 可以讓等待鎖的執行緒響應中斷,而使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;

  (4). 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到;

  (5). Lock可以提高多個執行緒進行讀操作的效率。

  在效能上來說,如果競爭資源不激烈,兩者的效能是差不多的。而當競爭資源非常激烈時(即有大量執行緒同時競爭),此時Lock的效能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。

三. 鎖的相關概念介紹

1、可重入鎖

  如果鎖具備可重入性,則稱作為 可重入鎖 。像 synchronized 和 ReentrantLock 都是可重入鎖,可重入性實際上表明瞭 鎖的分配粒度:基於執行緒的分配,而不是基於方法呼叫的分配。舉個簡單的例子,當一個執行緒執行到某個synchronized方法時,比如說method1,而在method1中會呼叫另外一個synchronized方法method2,此時執行緒不必重新去申請鎖,而是可以直接執行方法method2。

class MyClass {
    public synchronized void method1() {
        method2();
    }

    public synchronized void method2() {

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  上述程式碼中的兩個方法method1和method2都用synchronized修飾了。假如某一時刻,執行緒A執行到了method1,此時執行緒A獲取了這個物件的鎖,而由於method2也是synchronized方法,假如synchronized不具備可重入性,此時執行緒A需要重新申請鎖。但是,這就會造成死鎖,因為執行緒A已經持有了該物件的鎖,而又在申請獲取該物件的鎖,這樣就會執行緒A一直等待永遠不會獲取到的鎖。而由於synchronized和Lock都具備可重入性,所以不會發生上述現象。

2、可中斷鎖

  顧名思義,可中斷鎖就是可以響應中斷的鎖。在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。   如果某一執行緒A正在執行鎖中的程式碼,另一執行緒B正在等待獲取該鎖,可能由於等待時間過長,執行緒B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的執行緒中中斷它,這種就是可中斷鎖。在前面演示tryLock(long time, TimeUnit unit)和lockInterruptibly()的用法時已經體現了Lock的可中斷性。

3、公平鎖

  公平鎖即 儘量 以請求鎖的順序來獲取鎖。比如,同是有多個執行緒在等待一個鎖,當這個鎖被釋放時,等待時間最久的執行緒(最先請求的執行緒)會獲得該所,這種就是公平鎖。而非公平鎖則無法保證鎖的獲取是按照請求鎖的順序進行的,這樣就可能導致某個或者一些執行緒永遠獲取不到鎖。

  在Java中,synchronized就是非公平鎖(搶佔鎖),它無法保證等待的執行緒獲取鎖的順序。而對於ReentrantLock 和 ReentrantReadWriteLock,它預設情況下是非公平鎖,但是可以設定為 公平鎖(協同式執行緒排程)

看下面兩個例子:

Case : 公平鎖

public classRunFair {
    public static void main(String[] args) throws InterruptedException {
        final Service service = new Service(true);     // 公平鎖,設為 true
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("★執行緒" + Thread.currentThread().getName()
                        + "運行了");
                service.serviceMethod();
            }
        };

        Thread[] threadArray = new Thread[10];
        for (int i = 0; i < 10; i++) 
            threadArray[i] = new Thr