可重入鎖ReentrantLock在性能測試常見用法

在進行Java多線程編程的過程中,始終繞不開一個問題:線程安全。一般來說,我們可以通過對一些資源加鎖來實現,大多都是通過 synchronized 關鍵字實現。

在做性能測試時,如果TPS或者QPS要求沒有特別高, synchronized 一招鮮基本也能滿足大部分的需求了。

對於一招鮮無法很好解決的問題,就需要我們繼續探索 java.util.concurrent 包的其他內容。今天就分享一下 java.util.concurrent.locks.Lock 接口的實現類 java.util.concurrent.locks.ReentrantLock 的基本使用方法。

類功能概覽

java.util.concurrent.locks.Lock 接口支持三種方法的鎖獲取:阻塞鎖、可中斷鎖和超時鎖。

下面來分享這幾種鎖的常用的使用場景和案例。

阻塞鎖

方法是:java.util.concurrent.locks.ReentrantLock#lock,沒有參數。該方法會嘗試獲取鎖。當無法獲取鎖時,當前線程會處於休眠狀態,直到獲取鎖成功。

演示Demo如下:

private static final Logger log = LogManager.getLogger(LockTest.class);  
  
public static void main(String[] args) throws InterruptedException {  
    ReentrantLock lock = new ReentrantLock();  
    Thread lockTestThread = new Thread(() -> {  
        lock.lock();  
        log.info("獲取到鎖了!");  
        lock.unlock();  
    });  
    lock.lock();  
    lockTestThread.start(); 
    log.info("即將馬上釋放鎖!"); 
    Thread.sleep(1000);  
    lock.unlock();  
    lockTestThread.join();  
}

控制檯打印:

19:43:29 046 main 即將馬上釋放鎖!
19:43:30 050 Thread-2 獲取到鎖了!
19:43:30 uptime:1 s

由於異步線程獲取鎖的方法晚於 main 線程,所以會在獲取鎖的地方阻塞,直至 main 線程將鎖釋放。可以看到,兩條打印日誌相差約1s。

可中斷鎖

可中斷鎖API是:java.util.concurrent.locks.ReentrantLock#lockInterruptibly。該方式會嘗試獲取鎖,並且是阻塞的,但當未獲取到鎖時,如果當前線程被設置了中斷狀態,則會拋出 java.lang.InterruptedException 異常。

演示Demo如下:


private static final Logger log = LogManager.getLogger(LockTest.class);  
  
public static void main(String[] args) throws InterruptedException {  
    ReentrantLock lock = new ReentrantLock();  
    Thread lockTestThread = new Thread(() -> {  
        try {  
            lock.lockInterruptibly();  
            log.info("獲取到鎖了!");  
            lock.unlock();  
        } catch (InterruptedException e) {  
            log.warn("獲取鎖失敗!", e);  
        }  
  
    });  
    lock.lock();  
    lockTestThread.start();  
    lockTestThread.interrupt();  
    lock.unlock();  
    lockTestThread.join();  
}

控制檯打印:

19:58:21 250 Thread-2 獲取鎖失敗!
java.lang.InterruptedException: null
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220) ~[?:1.8.0_281]
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) ~[?:1.8.0_281]
	at com.funtest.temp.LockTest.lambda$main$0(LockTest.java:18) ~[classes/:?]
	at java.lang.Thread.run(Thread.java:748) [?:1.8.0_281]

超時鎖

超時鎖的API有兩個:java.util.concurrent.locks.ReentrantLock#tryLock()java.util.concurrent.locks.ReentrantLock#tryLock(long, java.util.concurrent.TimeUnit),返回1個Boolean值,表示獲取鎖是否成功。第二個API參數設置超時時間。這兩個API前者可以簡單理解爲後者時間設置爲0,獲取一下試試,成不成都返回結果。

演示Demo如下:

private static final Logger log = LogManager.getLogger(LockTest.class);  
  
public static void main(String[] args) throws InterruptedException {  
    ReentrantLock lock = new ReentrantLock();  
    Thread lockTestThread = new Thread(() -> {  
        boolean b = lock.tryLock();  
        log.info("第一次獲取鎖的結果:{}", b);  
        try {  
            boolean b1 = lock.tryLock(3, TimeUnit.SECONDS);  
            log.info("第二次獲取鎖的結果:{}", b1);  
        } catch (InterruptedException e) {  
            log.warn("第二次獲取鎖的時候被中斷了");  
        }  
    });  
    lock.lock();  
    lockTestThread.start();  
    Thread.sleep(1000);  
    lock.unlock();  
    lockTestThread.join();  
}

控制檯打印:

20:05:13 559 Thread-2 第一次獲取鎖的結果:false
20:05:14 563 Thread-2 第二次獲取鎖的結果:true
20:05:14 uptime:2 s

可以看到再等待了 1s 之後,第二次獲取鎖成功了。爲了簡化代碼,我並沒有寫判斷獲取鎖狀態的代碼。

最佳實踐

對於 java.util.concurrent.locks.ReentrantLock ,常用最佳實踐只有一個,非常容易掌握。那就是使用 try-catch-finally 語法實現,演示Demo如下:

boolean status = false;  
try {  
    status = lock.tryLock(3, TimeUnit.SECONDS);  
} catch (Exception e) {  
    // 異常處理  
} finally {  
    if (status) lock.unlock();  
}
  1. 儘量使用超時鎖
  2. 儘可能少佔用鎖
  3. 儘量低頻使用

可重入

java.util.concurrent.locks.ReentrantLock 直譯就是可重入鎖,意思是當一個線程獲取到鎖之後,還可以再獲取一次,當然釋放也需要兩次。在內部有專門用來計數的功能,當然也是線程安全的。

在性能測試實踐中,很少能遇到使用 可重入 的特性的場景。所以這裏建議不要過度使用 java.util.concurrent.locks.ReentrantLock,複雜場景可以有更加簡單可靠的解決方案。

公平鎖與非公平鎖

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值,含義既是是否使用公平鎖。無參的構造方法默認使用的非公平鎖。公平鎖和非公平鎖的主要區別是獲取鎖的方式不同。公平鎖的獲取是公平的,線程依次排隊獲取鎖。誰等待的時間最長,就由誰獲得鎖。非公平鎖獲取是隨機的,誰先請求誰先獲得鎖,不一定按照請求鎖的順序來。

具體區別如下:

  1. 獲取鎖的方式不同
  • 公平鎖:線程依次排隊獲取鎖,效率較低
  • 非公平鎖:隨機獲取鎖,效率較高
  1. 性能不同
  • 公平鎖:一次性喚醒隊列中等待時間最久的線程,Context Switching次數高,性能較低
  • 非公平鎖:隨機喚醒線程,Context Switching次數低,性能較高
  1. 鎖等待時間
  • 公平鎖:等待時間長,但訪問順序按隊列順序
  • 非公平鎖:等待時間短,但訪問順序隨機
  1. 影響因素
  • 公平鎖:隻影響當前等待的線程,不影響新來線程
  • 非公平鎖:可能會無限次讓新來線程搶佔鎖,導致老線程永遠獲取不到鎖
  1. 線程飢餓
  • 公平鎖:舊線程有獲取鎖的機會,相對更公平
  • 非公平鎖:可能導致線程飢餓問題

所以綜上,非公平鎖性能更高,但公平鎖更公平。由於性能測試中通常對性能是有要求的,若非強需求,建議儘量使用非公平鎖。

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