【連載 08】lock 鎖

FunTester發表於2024-12-31

2.3 lock 鎖

如果你曾經遭遇過執行緒不安全的問題,一定不會對 “鎖” 這個概念不陌生。實際上絕大多數執行緒安全的先解決方案都離不開 “鎖”。

JDK 裡面就有一個介面java.util.concurrent.locks.Lock,顧名思義,就是併發包中 “鎖”,大量的執行緒安全問題解決方案均是依賴這個介面的實現類。就跟 synchronized 關鍵字一樣,在效能測試實戰中只要掌握基本的功能和最佳實戰即可,這裡再重複一下上一節的建議:如需使用 Lock 實現的功能過於複雜,建議拋開 Lock,尋找更加簡單、可靠,已驗證的解決方案。

在效能測試中最常用的java.util.concurrent.locks.Lock實現類就是可重入鎖:java.util.concurrent.locks.ReentrantLock。相比synchronizedReentrantLock擁有以下主要優點:

可重入性。ReentrantLock允許已經獲取鎖的執行緒再次獲取鎖,相比 d 更加安全,避免發生死鎖的情況。

更加靈活。d 提供多個 API 完成鎖的獲取和釋放,讓使用者擁有更多選擇。

可中斷性。ReentrantLock功能中,獲取鎖的執行緒可以被主動中斷,相比synchronized無限等待,更加適合處理鎖的超時場景。

更高的效能。除了提供多種獲取鎖的 API 以外,ReentrantLock還提供兩種鎖型別:公平鎖和非公平鎖,幫助程式提升在加鎖場景的效能。

ReentrantLock提供了 3 中獲取鎖的 API,分別是:阻塞鎖、可中斷鎖和超時鎖。下面分別用程式碼演示如何使用。

2.3.1 阻塞鎖

獲取阻塞鎖的方法是:java.util.concurrent.locks.ReentrantLock#lock,沒有引數。該方法會嘗試獲取鎖。當無法獲取鎖時,當前執行緒會處於休眠狀態,直到獲取鎖成功。

演示程式碼如下:


package org.funtester.performance.books.chapter02.section3;

import java.util.concurrent.locks.ReentrantLock;

/**

 * 阻塞鎖示例

 */

public class BlockingLockDemo {

    public static void main(String[] args) throws InterruptedException {

        ReentrantLock lock = new ReentrantLock();// 建立一個可重入鎖

        Thread lockTestThread = new Thread(() -> {// 建立一個執行緒

            System.out.println(System.currentTimeMillis() + "  非同步執行緒啟動!  " + Thread.currentThread().getName());// 列印日誌

            lock.lock();// 獲取鎖

            System.out.println(System.currentTimeMillis() + "  獲取到鎖了!  " + Thread.currentThread().getName());// 列印日誌

            lock.unlock();// 釋放鎖

        });

        lock.lock();// 獲取鎖

        lockTestThread.start();// 啟動非同步執行緒

        Thread.sleep(100);// 睡眠100毫秒

        System.out.println(System.currentTimeMillis() + "  即將釋放鎖!  " + Thread.currentThread().getName());// 列印日誌

        lock.unlock();// 釋放鎖

    }

}

這個例子中,首先建立了一個非同步執行緒,執行程式碼邏輯為:獲取鎖,列印日誌,釋放鎖。然後在 main 執行緒中,先獲取鎖,再啟動非同步執行緒。然後main執行緒休眠 100 毫秒,再釋放鎖。控制檯輸出內容如下:

1698477535368  非同步執行緒啟動!  Thread-0

1698477535471  即將釋放鎖!  main

1698477535471  獲取到鎖了!  Thread-0

可以看到,非同步執行緒在啟動之後,等待了 100 毫秒才獲取到鎖,並列印日誌,且這個操作也是在 main 執行緒釋放鎖之後進行的。原因是因為 main 執行緒先於非同步執行緒獲取到鎖了,所以在 main 執行緒釋放鎖之前,非同步執行緒只能無所事事,乾等著。

阻塞鎖和synchronized解決執行緒安全思路和使用方法上比較相似,而且在效能測試工作中使用場景大多重合。阻塞鎖在一定程度上可以替代synchronized,特別是在編寫流程式的程式碼中。

2.3.2 可中斷鎖

可中斷鎖的獲取方法是:java.util.concurrent.locks.ReentrantLock#lockInterruptibly,沒有引數。該方式會嘗試獲取鎖,並且是阻塞的,但當未獲取到鎖時,如果當前執行緒被設定了中斷狀態,則會丟擲 java.lang.InterruptedException 異常。

下面是演示程式碼:

package org.funtester.performance.books.chapter02.section3;

import java.util.concurrent.locks.ReentrantLock;

/**

 * 可中斷鎖示例

 */

public class InterruptiblyLockDemo {

    public static void main(String[] args) throws InterruptedException {

        ReentrantLock lock = new ReentrantLock();// 建立一個可重入鎖

        Thread lockTestThread = new Thread(() -> {// 建立一個執行緒

            try {

                lock.lockInterruptibly();// 獲取鎖

                System.out.println(System.currentTimeMillis() + "  獲取到鎖了!  " + Thread.currentThread().getName());// 列印日誌

                lock.unlock();// 釋放鎖

            } catch (InterruptedException e) {

                System.out.println(System.currentTimeMillis() + "  執行緒被中斷了!  " + Thread.currentThread().getName());// 列印日誌

            }

        });

        lock.lock();// 獲取鎖

        lockTestThread.start();// 啟動非同步執行緒

        lockTestThread.interrupt();// 中斷非同步執行緒

        lock.unlock();// 釋放鎖

    }

}

在這個例子中,首先建立了一個非同步執行緒,執行程式碼邏輯為獲取鎖(可中斷),列印日誌,釋放鎖。然後讓 main 執行緒先獲取鎖,然後啟動非同步執行緒,再中斷非同步執行緒。下面來就控制檯輸出:

1698478061924 執行緒被中斷了! Thread-0

這裡看到只有一行輸出,即非同步執行緒再獲取鎖時被中斷了,丟擲的異常被捕獲。

可中斷鎖繼承了阻塞鎖的有點,提供了將執行緒從等待中解脫的方案,在使用上更加廣泛。可中斷鎖可以進行執行緒間超時控制、防止無限等待,可以非常優雅地關閉被阻塞的執行緒,釋放資源。可中斷鎖適合多執行緒協作的場景,要求使用者對多執行緒瞭解也更高。

2.3.3 超時鎖

超時鎖的獲取方法有兩個:java.util.concurrent.locks.ReentrantLock#tryLock()java.util.concurrent.locks.ReentrantLock#tryLock(long, java.util.concurrent.TimeUnit),返回Boolean值,表示獲取鎖是否成功。第二個 API 引數設定超時時間。這兩個 API 前者可以簡單理解為後者時間設定為 0,含義是嘗試獲取一次,返回結果。

演示程式碼如下:


package org.funtester.performance.books.chapter02.section3;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.locks.ReentrantLock;

/**

 * 超時鎖示例

 */

public class TimeoutLockDemo {

    public static void main(String[] args) throws InterruptedException {

        ReentrantLock lock = new ReentrantLock();// 建立一個可重入鎖

        Thread lockTestThread = new Thread(() -> {// 建立一個執行緒

            boolean b = lock.tryLock();// 第一次嘗試獲取鎖

            System.out.println(System.currentTimeMillis() + "  第一次獲取鎖的結果:" + b + "  " + Thread.currentThread().getName());// 列印日誌

            try {

                boolean b1 = lock.tryLock(3, TimeUnit.SECONDS);

                System.out.println(System.currentTimeMillis() + "  第二次獲取鎖的結果:" + b1 + "  " + Thread.currentThread().getName());

            } catch (InterruptedException e) {

                System.out.println(System.currentTimeMillis() + "  第二次獲取鎖中斷了  " + Thread.currentThread().getName());

            }

        });

        lock.lock();// 獲取鎖

        lockTestThread.start();// 啟動非同步執行緒

        Thread.sleep(100);// 睡眠100毫秒

        lock.unlock();// 釋放鎖

    }

}

在這個例子中,依舊先建立一個非同步執行緒,執行的邏輯為:首先嚐試獲取一次並且列印結果,然後第二次嘗試獲取,設定超時時間 3 秒,並列印結果。main 執行緒依舊先獲取鎖,然後啟動非同步執行緒,休眠 100 ms 然後釋放鎖。例子中,為了簡化程式碼,筆者並沒有編寫依據獲取鎖的結果釋放鎖的程式碼。控制檯輸出內容如下:

1698479430990  第一次獲取鎖的結果:false  Thread-0

1698479431090  第二次獲取鎖的結果:true  Thread-0

可以看到第一次獲取鎖失敗了,原因是該鎖正在被 main 執行緒持有。第二獲取鎖成功了,因為 main 執行緒持有鎖 100 毫秒之後便釋放鎖。在非同步執行緒第二次獲取鎖的 3 秒超時時間內,它成功了,所以獲取到了鎖。

在三種鎖的方法中,超時鎖在效能測試中使用最廣泛。它提供了一種簡單、可靠的控制鎖等待時間的方式。相比可中斷鎖,超時鎖對新手更加容易上手,無須掌握執行緒間統信、排程的知識。

2.3.4 公平鎖和非公平鎖

java.util.concurrent.locks.ReentrantLock 有一個構造方法如下

/** 

 * Creates an instance of {@code ReentrantLock} with the 

 * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy 

 */public ReentrantLock(boolean fair) { 

    sync = fair ? new FairSync() : new NonfairSync(); 

}

方法引數中Boolean值,含義即是否使用公平鎖。無參的構造方法預設使用的非公平鎖。公平鎖和非公平鎖的主要區別是獲取鎖的方式不同。公平鎖的獲取是公平的,執行緒依次排隊獲取鎖。誰等待的時間最長,就由誰獲得鎖。非公平鎖獲取是隨機的,誰先請求誰先獲得鎖,不一定按照請求鎖的順序來。ReentrantLock預設的是非公平鎖,相比公平鎖擁有更高效能。

2.3.5 最佳實戰

在效能測試實戰中, java.util.concurrent.locks.ReentrantLock而言 ,常用最佳實戰非常容易掌握。那就是使用 try-catch-finally 語法實現,演示案例如下:

boolean status = false; 

try { 

    status = lock.tryLock(3, TimeUnit.SECONDS); 

} catch (Exception e) { 

    // 異常處理 

} finally { 

    if (status) lock.unlock(); 

}

在使用ReentrantLock解決執行緒安全問題時,有幾點注意事項:

  • 必須主動進行鎖管理。與synchronized不同,ReentrantLock要求必需顯示獲取和釋放鎖,特別在釋放鎖時,最簡單的方法就是按照最佳實戰,將其放在 finally 中執行。
  • 竭力避免死鎖。不要混合使用不同鎖;不要在一個功能中使用過多的鎖和synchronized關鍵字;避免多次獲取鎖;使用使用 lockInterruptibly() 獲取鎖,如果在等待鎖的過程中執行緒被中斷,需要有處理程式碼進行後續處理。
  • 儘量使用 ReentrantLock 預設的非公平鎖。

雖然java.util.concurrent.locks.ReentrantLock叫可重入鎖,但是在效能測試實踐當中,不建議使用可重入功能。主要原因以下兩點:

  • (1)增加鎖競爭,影響效能。使用不當會導致同一個執行緒頻繁獲取和釋放鎖,增加競爭,降低程式效能。
  • (2)死鎖風險。如果同步程式碼中多次使用鎖,就需要嚴格釋放鎖流程,一旦發生異常而沒有捕獲處理,則會造成死鎖風險。

在筆者的效能測試生涯中,沒有必須使用可重入特性的場景,所以在效能測試實踐中,應當儘量避免使用該特性,防止異常情況發生。

書的名字:從 Java 開始做效能測試

如果本書內容對你有所幫助,希望各位不吝讚賞,讓我可以貼補家用。讚賞兩位數可以提前閱讀未公開章節。我也會嘗試製作本書的影片教程,包括必要的答疑。

FunTester 原創精華

【連載】從 Java 開始效能測試

  • 混沌工程、故障測試、Web 前端
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go
  • 白盒、工具、爬蟲、UI 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章