1. 程式人生 > >效能對比:ReentrantLock vs Synchronized

效能對比:ReentrantLock vs Synchronized

記一次併發相關的效能測試。

#起因

最近遇到高併發引起的效能問題,最終定位到的問題是 LinkedBlockingQueue 的效能不行,最終通過建立了多個 Queue 來減少每個 Queue 的競爭壓力。人生中第一次遇到 JDK 自帶資料結構無法滿足需求的情形,決心好好研究一下為什麼。

壓測在一個 40 個核的機器上,tomcat 預設 200 個執行緒,傳送方以 500 併發約 1w QPS 傳送請求,要求999 分位的響應在 50ms 左右。程式碼中有一個非同步寫入資料庫的任務,實際測試時有超過 60% 的延時都在寫入佇列中(實際上是往 ThreadPool 提交任務)。於是開始調研 LinkedBlockingQueue

的實現。

LinkedBlockingQueue 相當於是普通 LinkedList 加上 ReentrantLock 在操作時加鎖。而 ReentrantLock (以及其它 Java 中的鎖)內部都是靠 CAS 來實現原子性。而 CAS 在高併發時因為執行緒會不停重試,所以理論上效能會比原生的鎖更差。

#測試與結果

實際上想對比 CAS 和原生鎖是很困難的。Java 中沒有原生的鎖,而 synchronized 有 JDK 的各種優化,在一些低併發的情況下也用到了 CAS。對比過 synchronizedUnsafe.compareAndSwapInt 發現 CAS 被吊打。所以最後還是退而求其次對比 ReentrantLock

Synchronized 的效能。

一個執行緒競爭 ReentrantLock 失敗時,會被放到等待對列中,不會參與後續的競爭,因此 ReentrantLock 不能代表 CAS 在高併發下的表現。不過一般我們也不會直接使用 CAS,所以測試結果也湊合著看了。

測試使用的是 JMH 框架,號稱能測到毫秒級。執行的機器是 40 核的,因此至少能保證同時競爭的執行緒是 40 個(如果 CPU 核數不足,儘管執行緒數多,真正同時併發的量可能並不多)。JDK 1.8 下測試。

#自增操作

首先測試用 synchronizedReentrantLock 同步自增操作,測試程式碼如下:

@Benchmark
@Group("lock")@GroupThreads(4)public void lockedOp() { try { lock.lock(); lockCounter ++; } finally { lock.unlock(); }}@Benchmark@Group("synchronized")@GroupThreads(4)public void synchronizedOp() { synchronized (this) { rawCounter ++; }}

結果如下:

#連結串列操作

自增操作 CPU 時間太短,適當增加每個操作的時間,改為往 linkedList 插入一個數據。程式碼如下:

@Benchmark@Group("lock")@GroupThreads(2)public void lockedOp() {    try {        lock.lock();        lockQueue.add("event");        if (lockQueue.size() >= CLEAR_COUNT) {            lockQueue.clear();        }    } finally {        lock.unlock();    }}@Benchmark@Group("synchronized")@GroupThreads(2)public void synchronizedOp() {    synchronized (this) {        rawQueue.add("event");        if (rawQueue.size() >= CLEAR_COUNT) {            rawQueue.clear();        }    }}

結果如下:

#結果分析

  1. 可以看到 ReentrantLock 的效能還是要高於 Synchronized 的。
  2. 在 2 個執行緒時吞吐量達到最低,而 3 個執行緒反而提高了,推測是因為兩個執行緒競爭時一定會發生執行緒排程,而多個執行緒(不公平)競爭時有一些執行緒是可以直接從當前執行緒手中接過鎖的。
  3. 隨著執行緒數的增加,吞吐量只有少量的下降。首先推測因為同步程式碼最多隻有一個執行緒在執行,所以執行緒數雖然增多,吞吐量是不會增加多少的。其次是大部分執行緒變成等待後就不太會被喚醒,因此不太會參與後續的競爭。
  4. (linkedlist 測試中)持有鎖的時間增加後,ReentrantLock 與 Synchronized 的吞吐量差距減小了,應該是能佐證 CAS 執行緒重試的開銷在增長的。

這個測試讓我對 ReentrantLock 有了更多的信心,不過一般開發時還是建議用 synchronized, 畢竟大佬們還在不斷優化中(看到有文章說 JDK 9 中的 Lock 和 synchronized 已經基本持平了)。

如果有人知道怎麼更好地對比 CAS 和鎖的效能,歡迎留言~