亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

鎖的可重入性驗證

1. 前言

本節內容主要是對 Java 鎖的可重入性進行驗證,鎖的可重入性的設計是避免死鎖非常好的設計思想。本節內容的知識點如下:

  • 什么是鎖的可重入性,這是本節課程的基礎內容;
  • 了解可重入鎖與非可重入性鎖的不同之處,以凸顯可重入性鎖的優勢所在,為本節基礎內容;
  • 了解什么情況下使用可重入鎖,是本節的重點內容之一;
  • synchronized 關鍵字驗證鎖的可重入性試驗,為本節核心內容之一;
  • ReentrantLock 驗證鎖的可重入性試驗,為本節核心內容之一;

其實 synchronized 關鍵字與 ReentrantLock 都是 Java 常見的可重入鎖,本節內容使用 ReentrantLock 和 synchronized 來講解鎖的可重入性。

2. 什么是鎖的可重入性

定義:可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者 class),不會因為之前已經獲取過還沒釋放而阻塞。

Java 中 ReentrantLock 和 synchronized 都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖。

可重入鎖原理:可重入鎖的原理是在鎖內部維護一個線程標示,用來標示該鎖目前被哪個線程占用,然后關聯一個計數器。一開始計數器值為 0,說明該鎖沒有被任何線程占用。當一個線程獲取了該鎖時,計數器的值會變成 1,這時其他線程再來獲取該鎖時會發現鎖的所有者不是自己而被阻塞掛起。

但是當獲取了該鎖的線程再次獲取鎖時發現鎖擁有者是自己,就會把計數器值加+1, 當釋放鎖后計數器值-1。當計數器值為 0 時,鎖里面的線程標示被重置為 null,這時候被阻塞的線程會被喚醒來競爭獲取該鎖。

3. 可重入鎖與非可重入性鎖

Java 中 ReentrantLock 和 synchronized 都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖。

為了解釋可重入鎖與非可重入性鎖的區別與聯系,我們拿可重入鎖 ReentrantLock 和 非重入鎖 NonReentrantLock 進行簡單的分析對比。

相同點: ReentrantLock 和 NonReentrantLock 都繼承父類 AQS,其父類 AQS 中維護了一個同步狀態 status 來計數重入次數,status 初始值為 0。

不同點:當線程嘗試獲取鎖時,可重入鎖先嘗試獲取并更新 status 值,如果 status == 0 表示沒有其他線程在執行同步代碼,則把 status 置為 1,當前線程開始執行。

如果 status != 0,則判斷當前線程是否是獲取到這個鎖的線程,如果是的話執行 status+1,且當前線程可以再次獲取鎖。

而非可重入鎖是直接去獲取并嘗試更新當前 status 的值,如果 status != 0 的話會導致其獲取鎖失敗,當前線程阻塞,導致死鎖發生。

4. 什么情況下使用可重入鎖

我們先來看看如下代碼:同步方法 helloB 方法調用了同步方法 helloA。

public class DemoTest{
    public synchronized void helloA(){
        System.out.println("helloA");
    }
    public synchronized void helloB(){
        System.out.println("helloB");
        helloA();
    }
}

在如上代碼中,調用 helloB 方法前會先獲取內置鎖,然后打印輸出。之后調用 helloA 方法,在調用前會先去獲取內置鎖,如果內置鎖不是可重入的,那么調用線程將會一直被阻塞。

因此,對于同步方法內部調用另外一個同步方法的情況下,一定要使用可重入鎖,不然會導致死鎖的發生。

5. synchronized 驗證鎖的可重入性

為了更好的理解 synchronized 驗證鎖的可重入性,我們來設計一個簡單的場景。

場景設計

  • 創建一個類,該類中有兩個方法,helloA 方法和 helloB 方法;
  • 將兩個方法內部的邏輯進行 synchronized 同步;
  • helloA 方法內部調用 helloB 方法,營造可重入鎖的場景;
  • main 方法創建線程,調用 helloA 方法;
  • 觀察結果,看是否可以成功進行調用。

實例

public class DemoTest {
    public static void main(String[] args) {
        new Thread(new SynchronizedTest()). start();
    }
}
class SynchronizedTest implements Runnable {
    private final Object obj = new Object();
    public void helloA() { //方法1,調用方法2
        synchronized (obj) {
            System.out.println(Thread.currentThread().getName() + " helloA()");
            helloB();
        }
    }
    public void helloB() {
        synchronized (obj) {
            System.out.println(Thread.currentThread().getName() + " helloB()");
        }
    }
    @Override
    public void run() {
        helloA(); //調用helloA方法
    }
}

結果驗證

Thread-0 helloA()
Thread-0 helloB()

結果解析:如果同一線程,鎖不可重入的話,helloB 需要等待 helloA 釋放 obj 鎖,如此一來,helloB 無法進行鎖的獲取,最終造成無限等待,無法正常執行。此處說明了 synchronized 關鍵字的可重入性,因此能夠正常進行兩個方法的執行。

6. ReentrantLock 驗證鎖的可重入性

相同的場景,對代碼進行如下改造,將 synchronized 同步代碼塊修改成 lock 接口同步,我們看代碼實例如下:

public class DemoTest {
    public static void main(String[] args) {
        new Thread(new SynchronizedTest()). start();
    }
}
class SynchronizedTest implements Runnable {
    private final Lock lock = new ReentrantLock();
    public void helloA() { //方法1,調用方法2
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " helloA()");
            helloB();
        } finally {
            lock.unlock();
        }
    }
    public void helloB() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " helloB()");
        } finally {
            lock.unlock();
        }
    }
    @Override
    public void run() {
        helloA();
    }
}

結果驗證

Thread-0 helloA()
Thread-0 helloB()

結果解析:ReentrantLock 一樣是可重入鎖,試驗成功。

7. 小結

鎖的可重入性這一概念對于并發編程非常重要,對于本節內容需要深入的理解并掌握。我們之前已經學習過了 synchronized 關鍵字和 ReentrantLock 鎖,此處知識用兩者進行了可重入的驗證。

本節關鍵點在于可重入性的意義所在,需要結合實例進行更加細致的理解和掌握。