死磕 java同步系列之ReentrantLock VS synchronized——結果可能跟你想的不一樣

彤哥讀原始碼發表於2019-06-11

問題

(1)ReentrantLock有哪些優點?

(2)ReentrantLock有哪些缺點?

(3)ReentrantLock是否可以完全替代synchronized?

簡介

synchronized是Java原生提供的用於在多執行緒環境中保證同步的關鍵字,底層是通過修改物件頭中的MarkWord來實現的。

ReentrantLock是Java語言層面提供的用於在多執行緒環境中保證同步的類,底層是通過原子更新狀態變數state來實現的。

既然有了synchronized的關鍵字來保證同步了,為什麼還要實現一個ReentrantLock類呢?它們之間有什麼異同呢?

ReentrantLock VS synchronized

直接上表格:(手機橫屏檢視更方便)

功能 ReentrantLock synchronized
可重入 支援 支援
非公平 支援(預設) 支援
加鎖/解鎖方式 需要手動加鎖、解鎖,一般使用try..finally..保證鎖能夠釋放 手動加鎖,無需刻意解鎖
按key鎖 不支援,比如按使用者id加鎖 支援,synchronized加鎖時需要傳入一個物件
公平鎖 支援,new ReentrantLock(true) 不支援
中斷 支援,lockInterruptibly() 不支援
嘗試加鎖 支援,tryLock() 不支援
超時鎖 支援,tryLock(timeout, unit) 不支援
獲取當前執行緒獲取鎖的次數 支援,getHoldCount() 不支援
獲取等待的執行緒 支援,getWaitingThreads() 不支援
檢測是否被當前執行緒佔有 支援,isHeldByCurrentThread() 不支援
檢測是否被任意執行緒佔有 支援,isLocked() 不支援
條件鎖 可支援多個條件,condition.await(),condition.signal(),condition.signalAll() 只支援一個,obj.wait(),obj.notify(),obj.notifyAll()

對比測試

在測試之前,我們先預想一下結果,隨著執行緒數的不斷增加,ReentrantLock(fair)、ReentrantLock(unfair)、synchronized三者的效率怎樣呢?

我猜測應該是ReentrantLock(unfair)> synchronized > ReentrantLock(fair)。

到底是不是這樣呢?

直接上測試程式碼:(為了全面對比,彤哥這裡把AtomicInteger和LongAdder也拿來一起對比了)

public class ReentrantLockVsSynchronizedTest {
    public static AtomicInteger a = new AtomicInteger(0);
    public static LongAdder b = new LongAdder();
    public static int c = 0;
    public static int d = 0;
    public static int e = 0;

    public static final ReentrantLock fairLock = new ReentrantLock(true);
    public static final ReentrantLock unfairLock = new ReentrantLock();


    public static void main(String[] args) throws InterruptedException {
        System.out.println("-------------------------------------");
        testAll(1, 100000);
        System.out.println("-------------------------------------");
        testAll(2, 100000);
        System.out.println("-------------------------------------");
        testAll(4, 100000);
        System.out.println("-------------------------------------");
        testAll(6, 100000);
        System.out.println("-------------------------------------");
        testAll(8, 100000);
        System.out.println("-------------------------------------");
        testAll(10, 100000);
        System.out.println("-------------------------------------");
        testAll(50, 100000);
        System.out.println("-------------------------------------");
        testAll(100, 100000);
        System.out.println("-------------------------------------");
        testAll(200, 100000);
        System.out.println("-------------------------------------");
        testAll(500, 100000);
        System.out.println("-------------------------------------");
//        testAll(1000, 1000000);
        System.out.println("-------------------------------------");
        testAll(500, 10000);
        System.out.println("-------------------------------------");
        testAll(500, 1000);
        System.out.println("-------------------------------------");
        testAll(500, 100);
        System.out.println("-------------------------------------");
        testAll(500, 10);
        System.out.println("-------------------------------------");
        testAll(500, 1);
        System.out.println("-------------------------------------");
    }

    public static void testAll(int threadCount, int loopCount) throws InterruptedException {
        testAtomicInteger(threadCount, loopCount);
        testLongAdder(threadCount, loopCount);
        testSynchronized(threadCount, loopCount);
        testReentrantLockUnfair(threadCount, loopCount);
//        testReentrantLockFair(threadCount, loopCount);
    }

    public static void testAtomicInteger(int threadCount, int loopCount) throws InterruptedException {
        long start = System.currentTimeMillis();

        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                for (int j = 0; j < loopCount; j++) {
                    a.incrementAndGet();
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await();

        System.out.println("testAtomicInteger: result=" + a.get() + ", threadCount=" + threadCount + ", loopCount=" + loopCount + ", elapse=" + (System.currentTimeMillis() - start));
    }

    public static void testLongAdder(int threadCount, int loopCount) throws InterruptedException {
        long start = System.currentTimeMillis();

        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                for (int j = 0; j < loopCount; j++) {
                    b.increment();
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await();

        System.out.println("testLongAdder: result=" + b.sum() + ", threadCount=" + threadCount + ", loopCount=" + loopCount + ", elapse=" + (System.currentTimeMillis() - start));
    }

    public static void testReentrantLockFair(int threadCount, int loopCount) throws InterruptedException {
        long start = System.currentTimeMillis();

        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                for (int j = 0; j < loopCount; j++) {
                    fairLock.lock();
                    // 消除try的效能影響
//                    try {
                        c++;
//                    } finally {
                        fairLock.unlock();
//                    }
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await();

        System.out.println("testReentrantLockFair: result=" + c + ", threadCount=" + threadCount + ", loopCount=" + loopCount + ", elapse=" + (System.currentTimeMillis() - start));
    }

    public static void testReentrantLockUnfair(int threadCount, int loopCount) throws InterruptedException {
        long start = System.currentTimeMillis();

        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                for (int j = 0; j < loopCount; j++) {
                    unfairLock.lock();
                    // 消除try的效能影響
//                    try {
                        d++;
//                    } finally {
                        unfairLock.unlock();
//                    }
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await();

        System.out.println("testReentrantLockUnfair: result=" + d + ", threadCount=" + threadCount + ", loopCount=" + loopCount + ", elapse=" + (System.currentTimeMillis() - start));
    }

    public static void testSynchronized(int threadCount, int loopCount) throws InterruptedException {
        long start = System.currentTimeMillis();

        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                for (int j = 0; j < loopCount; j++) {
                    synchronized (ReentrantLockVsSynchronizedTest.class) {
                        e++;
                    }
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await();

        System.out.println("testSynchronized: result=" + e + ", threadCount=" + threadCount + ", loopCount=" + loopCount + ", elapse=" + (System.currentTimeMillis() - start));
    }

}

執行這段程式碼,你會發現結果大大出乎意料,真的是不測不知道,一測嚇一跳,執行後發現以下規律:

隨著執行緒數的不斷增加,synchronized的效率竟然比ReentrantLock非公平模式要高!

彤哥的電腦上大概是高3倍左右,我的執行環境是4核8G,java版本是8,請大家一定要在自己電腦上執行一下,並且最好能給我反饋一下。

彤哥又使用Java7及以下的版本執行了,發現在Java7及以下版本中synchronized的效率確實比ReentrantLock的效率低一些。

總結

(1)synchronized是Java原生關鍵字鎖;

(2)ReentrantLock是Java語言層面提供的鎖;

(3)ReentrantLock的功能非常豐富,解決了很多synchronized的侷限性;

(4)至於在非公平模式下,ReentrantLock與synchronized的效率孰高孰低,彤哥給出的結論是隨著Java版本的不斷升級,synchronized的效率只會越來越高;

彩蛋

既然ReentrantLock的功能更豐富,而且效率也不低,我們是不是可以放棄使用synchronized了呢?

答:我認為不是。因為synchronized是Java原生支援的,隨著Java版本的不斷升級,Java團隊也是在不斷優化synchronized,所以我認為在功能相同的前提下,最好還是使用原生的synchronized關鍵字來加鎖,這樣我們就能獲得Java版本升級帶來的免費的效能提升的空間。

另外,在Java8的ConcurrentHashMap中已經把ReentrantLock換成了synchronized來分段加鎖了,這也是Java版本不斷升級帶來的免費的synchronized的效能提升。

推薦閱讀

  1. 死磕 java同步系列之ReentrantLock原始碼解析(二)——條件鎖

  2. 死磕 java同步系列之ReentrantLock原始碼解析(一)——公平鎖、非公平鎖

  3. 死磕 java同步系列之AQS起篇

  4. 死磕 java同步系列之自己動手寫一個鎖Lock

  5. 死磕 java魔法類之Unsafe解析

  6. 死磕 java同步系列之JMM(Java Memory Model)

  7. 死磕 java同步系列之volatile解析

  8. 死磕 java同步系列之synchronized解析


歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。

qrcode

相關文章