Java的鎖—重入鎖(ReentrantLock)

重入鎖簡單理解就是對同一個線程而言,它可以重復的獲取鎖。例如這個線程可以連續獲取兩次鎖,但是釋放鎖的次數也一定要是兩次。下面是一個簡單例子:

public class ReenterLock {

    private static ReentrantLock lock = new ReentrantLock();

    private static int i = 0;

    // 循環1000000次
    private static Runnable runnable = () -> IntStream.range(0, 1000000).forEach((j) -> {
        lock.lock();
        try {
            i++;
        } finally {
            lock.unlock();
        }
    });

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        // 利用join,等thread1,thread2結束后,main線程才繼續運行,并打印 i
        thread1.join();
        thread2.join();
        // 利用lock保護的 i,最終結果為 2000000,如果不加,則值肯定小于此數值
        System.out.println(i);
    }
}

從上面的代碼可以看到,相比于synchronized,開發者必須手動指定鎖的位置和什么時候釋放鎖,這樣必然增加了靈活性。

線程中斷響應

如果線程阻塞于synchronized,那么要么獲取到鎖,繼續執行,要么一直等待。重入鎖提供了另一種可能,就是中斷線程。下面的例子是利用兩個線程構建一個死鎖,然后中斷其中一個線程,使另一個線程獲取鎖的例子:

public class ReenterLockInterrupt {
    private static ReentrantLock lock = new ReentrantLock();

    private static Runnable runnable = () -> {
        try {
            // 利用 lockInterruptibly 申請鎖,這是可以進中斷申請的申請鎖操作
            lock.lockInterruptibly();
            // 睡眠20秒,在睡眠結束之前,main方法里要中斷thread2的獲取鎖操作
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            String threadName = Thread.currentThread().getName();
            // 中斷后拋出異常,最后要釋放鎖
            // 如果是線程1則釋放鎖,因為線程2就沒拿到鎖,所以不用釋放
            if ("Thread-1".equals(threadName)) lock.unlock();
            System.out.println(threadName+" 停止");
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread-1");
        Thread thread2 = new Thread(runnable, "thread-2");
        thread1.start();

        // 讓主線程停一下,讓thread1獲取鎖后再啟動thread2
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // 這里什么也不做
        }

        thread2.start();
        thread2.interrupt();
    }
}

thread-1拿到鎖之后,線程即持有鎖并等待20秒,然后thread-2啟動,并沒有拿到鎖,這時候中斷thread-2線程,線程2退出。

有限時間的等待鎖

顧名思義,簡單理解就是在指定的時間內如果拿不到鎖,則不再等待鎖。當持有鎖的線程出問題導致長時間持有鎖的時候,你不可能讓其他線程永遠等待其釋放鎖。下面是一個例子:

public class ReenterTryLock {
    private static ReentrantLock reenterLock = new ReentrantLock();

    private static Runnable runnable = () -> {
        try {
            // tryLock()方法會返回一個布爾值,獲取鎖成功則為true
            if (reenterLock.tryLock(3, TimeUnit.SECONDS)) {
                Thread.sleep(5000);
            } else {
                System.out.println(Thread.currentThread().getName() + "獲取鎖失敗");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 最后,如果當前前程在持有鎖,則釋放鎖
            if (reenterLock.isHeldByCurrentThread()) {
                System.out.println(Thread.currentThread().getName() + "釋放鎖了");
                reenterLock.unlock();
            }
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread-1");
        Thread thread2 = new Thread(runnable, "thread-2");

        thread1.start();
        thread2.start();
    }
}

這里使用tryLock()第一個獲取鎖的線程,會停止5秒。而獲取鎖的設置為3秒獲取不到鎖則放棄,所以第二個去嘗試獲取鎖的線程是獲取不到鎖而被迫停止的。如果tryLock()方法不傳入任何參數,那么獲取鎖的線程不會等待鎖,則立即返回false。

公平鎖與非公平鎖

當一個線程釋放鎖時,其他等待的線程則有機會獲取鎖,如果是公平鎖,則分先來后到的獲取鎖,如果是非公平鎖則誰搶到鎖算誰的,這就相當于排隊買東西和不排隊買東西是一個道理。Java的synchronized關鍵字就是非公平鎖

那么重入鎖ReentrantLock()是公平鎖還是非公平鎖?

重入鎖ReentrantLock()是可以設置公平性的,可以參考其構造方法:

// 通過傳入一個布爾值來設置公平鎖,為true則是公平鎖,false則為非公平鎖
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

構建一個公平鎖需要維護一個有序隊列,如果實際需求用不到公平鎖則不需要使用公平鎖。下面用一個例子來演示公平鎖與非公平鎖的區別:

public class ReenterTryLockFair {
    // 分別設置公平鎖和非公平鎖,分析打印結果
    private static ReentrantLock lock = new ReentrantLock(true);

    private static Runnable runnable = () -> {
        while (true) {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " 獲取了鎖");
            } finally {
                lock.unlock();
            }
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(runnable, "thread---1");
        Thread thread2 = new Thread(runnable, "thread---2");
        Thread thread3 = new Thread(runnable, "thread---3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

當設置為true即公平鎖的時候,可以看到打印非常規律,截取一段兒打印結果:

thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖
thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖
thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖
thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖
thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖
thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖

可以看到,都是thread--1,thread--2,thread--3,無限循環下去,如果設置的為非公平鎖,打印結果就混亂沒有規律了:

thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---1 獲取了鎖

Condition

同jdk中的等待/通知機制類似,只不過Condition是用在重入鎖這里的。有了Condition,線程就可以在合適的時間等待,在合適的時間繼續執行。

Condition接口包含以下方法:

// 讓當前線程等待,并釋放鎖
void await() throws InterruptedException;
// 和await類似,但在等待過程中不會相應中斷
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
// 喚醒等待中的線程
void signal();
// 喚醒等待中的所有線程
void signalAll();

下面是一個簡單示例:

public class ReenterLockCondition {
    private static ReentrantLock lock = new ReentrantLock();

    private static Condition condition = lock.newCondition();

    private static Runnable runnable = () -> {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "進入等待。。");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "繼續執行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    };

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(runnable, "thread--1");
        thread.start();

        Thread.sleep(2000);

        lock.lock();
        condition.signal();
        System.out.println("主線程發出信號");
        lock.unlock();
    }
}

thread--1啟動,拿到鎖,然后進入等待并且釋放鎖,2秒后,主線程拿到鎖,然后發出信號并釋放鎖,最后,thread--1繼續執行。下面是打印結果:

thread--1進入等待。。
主線程發出信號
thread--1繼續執行
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容