重入鎖:ReentrantLock 詳解

在JDK5.0版本之前,重入鎖的性能遠遠好於synchronized關鍵字,JDK6.0版本之後synchronized 得到了大量的優化,二者性能也不分伯仲,但是重入鎖是可以完全替代synchronized關鍵字的。除此之外,重入鎖又自帶一系列高逼格UBFF:可中斷響應、鎖申請等待限時、公平鎖。另外可以結合Condition來使用,使其更是逼格滿滿。

先來盤花生米:

package somhu;import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockTest implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();    public static int i = 0;    @Override
    public void run() {        for (int j = 0; j < 10000; j++) {
            lock.lock();  // 看這裏就可以
            //lock.lock(); ①
            try {
                i++;
            } finally {
                lock.unlock(); // 看這裏就可以
                //lock.unlock();②
            }
        }
    }    public static void main(String[] args) throws InterruptedException {
        ReentrantLockTest test = new ReentrantLockTest();
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
        t1.start();t2.start();
        t1.join(); t2.join(); // main線程會等待t1和t2都運行完再執行以後的流程
        System.err.println(i);
    }
}

從上可以看出,使用重入鎖進行加鎖是一種顯式操作,通過何時加鎖與釋放鎖使重入鎖對邏輯控制的靈活性遠遠大於synchronized關鍵字。同時,需要注意,有加鎖就必須有釋放鎖,而且加鎖與釋放鎖的分數要相同,這裏就引出了“重”字的概念,如上邊代碼演示,放開①、②處的註釋,與原來效果一致。

硬菜來了:

1、中斷響應
對於synchronized塊來說,要麼獲取到鎖執行,要麼持續等待。而重入鎖的中斷響應功能就合理地避免了這樣的情況。比如,一個正在等待獲取鎖的線程被“告知”無須繼續等待下去,就可以停止工作了。直接上代碼,來演示使用重入鎖如何解決死鎖:12
package somhu;import java.util.concurrent.locks.ReentrantLock;public class KillDeadlock implements Runnable{
    public static ReentrantLock lock1 = new ReentrantLock();    public static ReentrantLock lock2 = new ReentrantLock();    int lock;    public KillDeadlock(int lock) {        this.lock = lock;
    }    @Override
    public void run() {        try {            if (lock == 1) {
                lock1.lockInterruptibly();  // 以可以響應中斷的方式加鎖
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
                lock2.lockInterruptibly();
            } else {
                lock2.lockInterruptibly();  // 以可以響應中斷的方式加鎖
                try {
                    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.err.println(Thread.currentThread().getId() + "退出!");
        }
    }    public static void main(String[] args) throws InterruptedException {
        KillDeadlock deadLock1 = new KillDeadlock(1);
        KillDeadlock deadLock2 = new KillDeadlock(2);
        Thread t1 = new Thread(deadLock1);
        Thread t2 = new Thread(deadLock2);
        t1.start();t2.start();
        Thread.sleep(1000);
        t2.interrupt(); // ③
    }
}

t1、t2線程開始運行時,會分別持有lock1和lock2而請求lock2和lock1,這樣就發生了死鎖。但是,在③處給t2線程狀態標記爲中斷後,持有重入鎖lock2的線程t2會響應中斷,並不再繼續等待lock1,同時釋放了其原本持有的lock2,這樣t1獲取到了lock2,正常執行完成。t2也會退出,但只是釋放了資源並沒有完成工作。

2、鎖申請等待限時

可以使用 tryLock()或者tryLock(long timeout, TimeUtil unit) 方法進行一次限時的鎖等待。

前者不帶參數,這時線程嘗試獲取鎖,如果獲取到鎖則繼續執行,如果鎖被其他線程持有,則立即返回 false ,也就是不會使當前線程等待,所以不會產生死鎖。
後者帶有參數,表示在指定時長內獲取到鎖則繼續執行,如果等待指定時長後還沒有獲取到鎖則返回false。

上代碼:

package somhu;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantLock;public class TryLockTest implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();    @Override
    public void run() {        try {            if (lock.tryLock(1, TimeUnit.SECONDS)) { // 等待1秒
                Thread.sleep(2000);  //休眠2秒
            } else {
                System.err.println(Thread.currentThread().getName() + "獲取鎖失敗!");
            }
        } catch (Exception e) {            if (lock.isHeldByCurrentThread()) lock.unlock();
        }
    }    public static void main(String[] args) throws InterruptedException {
        TryLockTest test = new TryLockTest();
        Thread t1 = new Thread(test); t1.setName("線程1");
        Thread t2 = new Thread(test); t1.setName("線程2");
        t1.start();t2.start();
    }
}/**
 * 運行結果:
 * 線程2獲取鎖失敗!
 */

上述示例中,t1先獲取到鎖,並休眠2秒,這時t2開始等待,等待1秒後依然沒有獲取到鎖,就不再繼續等待,符合預期結果。

3、公平鎖

所謂公平鎖,就是按照時間先後順序,使先等待的線程先得到鎖,而且,公平鎖不會產生飢餓鎖,也就是隻要排隊等待,最終能等待到獲取鎖的機會。使用重入鎖(默認是非公平鎖)創建公平鎖:

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}123

上代碼:

package somhu;import java.util.concurrent.locks.ReentrantLock;public class FairLockTest implements Runnable{
    public static ReentrantLock lock = new ReentrantLock(true);    @Override
    public void run() {        while (true) {            try {
                lock.lock();
                System.err.println(Thread.currentThread().getName() + "獲取到了鎖!");
            } finally {
                lock.unlock();
            }
        }
    }    public static void main(String[] args) throws InterruptedException {
        FairLockTest test = new FairLockTest();
        Thread t1 = new Thread(test, "線程1");
        Thread t2 = new Thread(test, "線程2");
        t1.start();t2.start();
    }
}/**
 * 運行結果:
 * 線程1獲取到了鎖!
 * 線程2獲取到了鎖!
 * 線程1獲取到了鎖!
 * 線程2獲取到了鎖!
 * 線程1獲取到了鎖!
 * 線程2獲取到了鎖!
 * 線程1獲取到了鎖!
 * 線程2獲取到了鎖!
 * 線程1獲取到了鎖!
 * 線程2獲取到了鎖!
 * 線程1獲取到了鎖!
 * 線程2獲取到了鎖!
 * 線程1獲取到了鎖!
 * 線程2獲取到了鎖!
 * 線程1獲取到了鎖!
 * 線程2獲取到了鎖!
 * ......(上邊是截取的一段)
 */

可以發現,t1和t2交替獲取到鎖。如果是非公平鎖,會發生t1運行了許多遍後t2纔開始運行的情況。

ReentrantLock 配合 Conditond 使用

配合關鍵字synchronized使用的方法如:await()、notify()、notifyAll(),同樣配合ReentrantLock 使用的Conditon提供了以下方法:

public interface Condition {
    void await() throws InterruptedException; // 類似於Object.wait()
    void awaitUninterruptibly(); // 與await()相同,但不會再等待過程中響應中斷
    long awaitNanos(long nanosTimeout) throws InterruptedException;    boolean await(long time, TimeUnit unit) throws InterruptedException;    boolean awaitUntil(Date deadline) throws InterruptedException;    void signal(); // 類似於Obejct.notify()
    void signalAll();
}123456789

ReentrantLock  實現了Lock接口,可以通過該接口提供的newCondition()方法創建Condition對象:

public interface Lock {
    void lock();    void lockInterruptibly() throws InterruptedException;    boolean tryLock();    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;    void unlock();
    Condition newCondition();
}12345678

上代碼:

package somhu;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockWithConditon implements Runnable{
    public static ReentrantLock lock = new ReentrantLock(true);    public static Condition condition = lock.newCondition();    @Override
    public void run() {
        lock.newCondition();        try {
            lock.lock();
            System.err.println(Thread.currentThread().getName() + "-線程開始等待...");
            condition.await();
            System.err.println(Thread.currentThread().getName() + "-線程繼續進行了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }    public static void main(String[] args) throws InterruptedException {
        ReentrantLockWithConditon test = new ReentrantLockWithConditon();
        Thread t = new Thread(test, "線程ABC");
        t.start();
        Thread.sleep(1000);
        System.err.println("過了1秒後...");
        lock.lock();
        condition.signal(); // 調用該方法前需要獲取到創建該對象的鎖否則會產生
                            // java.lang.IllegalMonitorStateException異常
        lock.unlock();
    }
}

好了,到這裏重入鎖ReentrantLock的基本使用方法就介紹完成了!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章