我們都使用第三方庫作為開發的正常部分。 通常,我們無法控制其內部。 JDK隨附的庫是一個典型示例。 這些庫中的許多庫都使用鎖來管理競爭。
JDK鎖具有兩種實現。 人們使用原子CAS樣式指令來管理索賠過程。 CAS指令往往是最昂貴的CPU指令類型,并且在x86上具有內存排序語義。 鎖通常是無競爭的,這會導致可能的優化,從而可以使用避免使用原子指令的技術將鎖偏向無競爭的線程。 這種偏向使得理論上的鎖定可以被同一線程快速重新獲得。 如果該鎖最終被多個線程爭用,則該算法將從偏見中恢復過來,并使用原子指令退回到標準方法。 偏向鎖定已成為Java 6的默認鎖定實現 。
在遵守單一作者原則時,偏向鎖定應該是您的朋友。 最近,當使用套接字API時,我決定衡量鎖定成本,并對結果感到驚訝。 我發現我的無競爭線程所產生的開銷比我預期的要多。 我匯總了以下測試,以比較Java 6中可用的當前鎖實現的成本。
考試
為了進行測試,我將在鎖中增加一個計數器,并增加鎖中競爭線程的數量。 對于Java可用的3種主要鎖實現,將重復此測試:
- Java語言監視器上的原子鎖定
- Java語言監視器上的偏向鎖定
- Java 5中隨java.util.concurrent包引入的ReentrantLock 。
我還將在最新的3代Intel CPU上運行測試。 對于每個CPU,我將執行測試,直到核心計數將支持的最大并發線程數為止。
該測試是在64位Linux(Fedora Core 15)和Oracle JDK 1.6.0_29上進行的。
編碼
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.CyclicBarrier;import static java.lang.System.out;public final class TestLocks implements Runnable
{public enum LockType { JVM, JUC }public static LockType lockType;public static final long ITERATIONS = 500L * 1000L *1000L;public static long counter = 0L;public static final Object jvmLock = new Object();public static final Lock jucLock = new ReentrantLock();private static int numThreads;private static CyclicBarrier barrier;public static void main(final String[] args) throws Exception{lockType = LockType.valueOf(args[0]);numThreads = Integer.parseInt(args[1]);runTest(numThreads); // warm upcounter = 0L;final long start = System.nanoTime();runTest(numThreads);final long duration = System.nanoTime() - start;out.printf("%d threads, duration %,d (ns)\n", numThreads, duration);out.printf("%,d ns/op\n", duration / ITERATIONS);out.printf("%,d ops/s\n", (ITERATIONS * 1000000000L) / duration);out.println("counter = " + counter);}private static void runTest(final int numThreads) throws Exception{barrier = new CyclicBarrier(numThreads);Thread[] threads = new Thread[numThreads];for (int i = 0; i < threads.length; i++){threads[i] = new Thread(new TestLocks());}for (Thread t : threads){t.start();}for (Thread t : threads){t.join();}}public void run(){try{barrier.await();}catch (Exception e){// don't care}switch (lockType){case JVM: jvmLockInc(); break;case JUC: jucLockInc(); break;}}private void jvmLockInc(){long count = ITERATIONS / numThreads;while (0 != count--){synchronized (jvmLock){++counter;}}}private void jucLockInc(){long count = ITERATIONS / numThreads;while (0 != count--){jucLock.lock();try{++counter;}finally{jucLock.unlock();}}}
}
編寫測試腳本:
設置-x
對于{1..8}中的i; 做Java -XX:-UseBiasedLocking TestLocks JVM $ i; 做完了 對于{1..8}中的i; 做Java -XX:+ UseBiasedLocking TestLocks JVM $ i; 做完了 對于{1..8}中的i; 做Java TestLocks JUC $ i; 做完了
結果
![]() |
圖1 |
![]() |
圖2 |
![]() |
圖3 |
在現代英特爾處理器上,偏置鎖定不再應該是默認的鎖定實現。 我建議您使用-XX:-UseBiasedLocking JVM選項來評估您的應用程序和實驗,以確定是否可以從針對無競爭情況使用基于原子鎖的算法中受益。
觀察結果
- 在無競爭的情況下,有偏鎖比原子鎖貴10%。 似乎對于最近的CPU代來說,原子指令的成本比偏向鎖的必要內務處理要少。 在Nehalem之前,鎖定指令會在內存總線上聲明一個鎖定以執行這些原子操作,每條操作將花費100個以上的周期。 自Nehalem以來,原子指令可以在CPU內核本地進行處理,并且在執行內存排序語義時不需要等待存儲緩沖區為空時,通常只需花費10-20個周期。
- 隨著爭用的增加,語言監視器鎖定將Swift達到吞吐量限制,而與線程數無關。
- 與使用同步的語言監視器相比,ReentrantLock提供了最佳的無競爭性能,并且隨著爭用的增加,擴展性也顯著提高。
- 當2個線程競爭時,ReentrantLock具有降低性能的奇怪特征。 這值得進一步調查。
- 當競爭線程數較少時,Sandybridge遭受原子指令增加的延遲 ,這在上一篇文章中已詳細介紹。 隨著競爭線程數的不斷增加,內核仲裁的成本趨于占主導地位,而Sandybridge則顯示出其在提高內存吞吐量方面的優勢。
結論
在開發自己的并發庫時,如果無鎖替代算法不是可行的選擇,則建議使用ReentrantLock而不是使用synced關鍵字,因為它在x86上具有明顯更好的性能。
更新2011年11月20日
Dave Dice指出,未對JVM啟動的前幾秒中創建的鎖實施偏向鎖。 我將在本周重新運行測試并發布結果。 我收到了更多質量反饋,表明我的結果可能無效。 微型基準測試可能會很棘手,但是在大型應用中衡量自己的應用程序的建議仍然存在。
考慮到Dave的反饋,可以在此后續博客中查看測試的重新運行。
參考:來自我們的JCG合作伙伴 Martin Thompson的Java鎖實現,來自Mechanical Sympathy Blog。
翻譯自: https://www.javacodegeeks.com/2012/07/java-lock-implementations.html