Skip to content

Latest commit

 

History

History
348 lines (248 loc) · 8.83 KB

ReentrantLock.md

File metadata and controls

348 lines (248 loc) · 8.83 KB

synchronized() 的替代品: ReentrantLock




ReentrantLock 基本使用


JDK 1.5 開始 ReentrantLock (重入鎖)就可以完全替代 synchronized() 了,且性能遠遠好於後者。JDK 1.6 開始, synchronized() 做了大量優化,使二者行能差距不大。

簡單的一個使用範例:


public class ReenterLock implements Runnable {

    public static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;

    @Override
    public void run() {
        for (int j = 0; j < 100000; j++){
            lock.lock();  // #1
            try {
                i++;
            }finally {
                lock.unlock();  // #2
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReenterLock lockTask = new ReenterLock();
        Thread t1 = new Thread(lockTask);
        Thread t2 = new Thread(lockTask);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

可以看到 #1 #2 處的操作,重入鎖有著明顯的操作上鎖解鎖的過程。我們必須手動控制上鎖解鎖,所以有好有壞,好處是我們可以靈活控制鎖邏輯,壞處是要時刻記得必須釋放鎖,不然其他 Thread 就沒機會訪問資源了。

至於為何 ReentrantLock 是 Reentrant(重入)呢?因為 ReentrantLock 可以重複上鎖:


lock.lock();
lock.lock();
try{
    i++;
}finally{
    lock.unlock();
    lock.unlock();
}

同一個 Thread 可以連續兩次獲得同一把鎖,要注意的是,如果同一個 Thread 多次上鎖,納在釋放鎖時也必須釋放相同次數,否則無法解鎖。



ReentrantLock 高級功能


ReentrantLock 還提供一些高級功能,像是:


  • 中斷等待

  • 鎖申請等待時間

  • 公平鎖


下面一一做介紹。



中斷等待


對於 synchronized() 來說,如果一個 Thread 在等待鎖,那結果只有 2 種可能,要馬它得到鎖繼續執行,要馬它繼續等待。使用 ReentrantLock 的話則會出現另一種可能,就是 被中斷。也就是說在等待狀態下,我們可以根據需求取消對鎖的請求。使用情形就是當一個 Thread 正在等待鎖,那麼它依然可以收到一個通知,被告知無須繼續等待,可以停止作業了,這種情況對於處理 dead lock 有一定的幫助。


下面示範一個 dead lock,然後用等待中斷來解決這個問題:


public class DeadLockDemo implements Runnable {

    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    public DeadLockDemo(int lock){
        this.lock = lock;
    }

    @Override
    public void run() {
        try {

            if (lock == 1){
                lock1.lockInterruptibly();
                try {
                    // 等待 0.5 秒再去嘗試取得 lock2 鎖。
                    Thread.sleep(500); 
                } catch (InterruptedException e){}
                lock2.lockInterruptibly();
            } else {
                lock2.lockInterruptibly();
                try {
                    // 等待 0.5 秒再去嘗試取得 lock1 鎖。
                    Thread.sleep(500);
                }catch (InterruptedException e){}
                lock1.lockInterruptibly();
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock1.isHeldByCurrentThread()){
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()){
                lock2.unlock();
            }
            System.out.println(Thread.currentThread().getName() + " 退出。");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DeadLockDemo task1 = new DeadLockDemo(1);
        DeadLockDemo task2 = new DeadLockDemo(2);
        Thread t1 = new Thread(task1,"Thread-1");
        Thread t2 = new Thread(task2,"Thread-2");
        t1.start();
        t2.start();
        Thread.sleep(2000);
        t2.interrupt();
    }
}

印出結果:


java.lang.InterruptedException
	at ...
Thread-2 退出。
Thread-1 退出。

lockInterruptibly() 方法代表這是一個可以被中斷的上鎖,只要觸發 interrupt() 就直接退出資源競爭,執行 finally{ } 中的解鎖邏輯。



鎖申請等待時間


除了等待中斷通知外,要避免 dead lock 還有一種方法,那就是限時等待時間。給定一個時間,讓 Thread 自動放棄。tryLock() 提供這樣方便的功能。


public class TimeLock implements Runnable {

    public static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            if (lock.tryLock(3, TimeUnit.SECONDS)){ // #1
                Thread.sleep(4000);
                System.out.println(Thread.currentThread().getName() + " done task.");
            } else {
                System.out.println(Thread.currentThread().getName() + " get lock failed.");
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        } finally {
            if(lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        TimeLock task = new TimeLock();
        Thread t1 = new Thread(task, "Thread-1");
        Thread t2 = new Thread(task, "Thread-2");
        t1.start();
        t2.start();
    }
}

印出結果:


Thread-2 get lock failed.
Thread-1 done task.

#1 處 tryLock() 接收 2 個參數,一個是等待時間,一個是計時單位。表示在這個鎖請求中,最多等待 3 秒,超過 3 秒沒取得鎖就返回 false,成功取得鎖就返回 true。

tryLock() 方法也可以不帶參數執行。在這種情況下當前 Thread 會嘗試取得鎖,如果鎖沒有被占用則申請成功,並返回 true。如果鎖被其他 Thread 占用則不進行等待,馬上返回 false。

由於篇幅原因,這裡有一個使用 tryLock() 解決 deal lock 的範例放在下面連結,就不展示在正文了。


TryLock.java



公平鎖


大多情況下,鎖的申請都是非公平的,也就是說 Thread-1 先申請 Lock-A,接著 Thread-2 也申請 Lock-A。那麼是 Thread-1 還是 Thread-2 獲得鎖是不確定的,系統只會從 Lock-A 的等待隊列中隨機挑選一個,因此沒有公平性可言。我們使用的 synchronized() 進行鎖控制就是這種非公平鎖。

ReentrantLock 允許我們對鎖公平進行設定。公平鎖會按照時間先後順序,保證先到先得。公平鎖一大特點是,它不會產生飢餓現象,只要你排隊,最終一定可以取得資源。


ReentrantLock 建構式中有這樣一個建構函式:


public ReentrantLock(boolean fair)

fairtrue 時,表示使用公平鎖。但是公平鎖實現成本比較高,因為內部要維護一個有序隊列。因此默認形況下都是使用非公平鎖。如果不是特別需求,這邊也建議不要用公平鎖。

下面示範一下公平鎖如何使用:


public class FairLock implements Runnable {

    public static ReentrantLock fairLock = new ReentrantLock(true);

    @Override
    public void run() {
        while(true) {
            try {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName() + " 獲得鎖。");
            } finally {
                fairLock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        FairLock r1 = new FairLock();
        Thread t1 = new Thread(r1, "Thread-1");
        Thread t2 = new Thread(r1, "Thread-2");
        t1.start();
        t2.start();
    }
}

印出部分結果:


...
Thread-1 獲得鎖。
Thread-2 獲得鎖。
Thread-1 獲得鎖。
Thread-2 獲得鎖。
Thread-1 獲得鎖。
Thread-2 獲得鎖。
Thread-1 獲得鎖。
Thread-2 獲得鎖。
...

可以看到兩個 Thread 會交替取得鎖,幾乎不會發生同一個 Thread 多次獲得鎖的狀況。


非公平鎖情況下,根據系統調度,一個 Thread 會傾向再次獲取已持有的鎖。這種分配方法是高效的,但無公平性可言。





總結一下 ReentrantLock 幾個重要方法:

  • lock() 取得鎖,若鎖被占用則等待。

  • lockInterruptibly() 取得鎖,等待時可被中斷。

  • tryLock() 嘗試獲得鎖,若成功返回 true,失敗返回 false。該方法不等待,立刻返回。

  • tryLock(long time, TimeUnit unit) 在給定時間內嘗試獲取鎖。

  • unlock() 釋放鎖。