和朱曄一塊兒複習Java併發(三):鎖(含鎖性能測試)

這個專題我發現怎麼慢慢演化爲性能測試了,遇到任何東西我就忍不住去測一把。本文咱們會大概看一下各類鎖數據結構的簡單用法,順便也會來比拼一下性能。java

各類併發鎖

首先,咱們定一個抽象基類,用於各類鎖測試的一些公共代碼:git

  • 咱們須要使用鎖來保護counter和hashMap這2個資源
  • write字段表示這個線程是執行寫操做仍是讀操做
  • 每個線程都會執行loopCount次讀或寫操做
  • start的CountDownLatch用於等待全部線程一塊兒執行
  • finish的CountDownLatch用於讓主線程等待全部線程都完成
@Slf4j
abstract class LockTask implements Runnable {
    protected volatile static long counter;
    protected boolean write;
    protected static HashMap<Long, String> hashMap = new HashMap<>();
    int loopCount;
    CountDownLatch start;
    CountDownLatch finish;

    public LockTask(Boolean write) {
        this.write = write;
    }

    @Override
    public void run() {
        try {
            start.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < loopCount; i++) {
            doTask();
        }
        finish.countDown();
    }

    abstract protected void doTask();
}
複製代碼

下面咱們實現最簡單的使用synchronized來實現的鎖,拿到鎖後咱們針對hashMap和counter作一下最簡單的操做:github

@Slf4j
class SyncTask extends LockTask {
    private static Object locker = new Object();

    public SyncTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        synchronized (locker) {
            if (write) {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } else {
                hashMap.get(counter);
                //log.debug("{}, {}", this.getClass().getSimpleName(), value);
            }
        }
    }
}
複製代碼

而後是ReentrantLock,使用也是很簡單,須要在finally中釋放鎖:bash

@Slf4j
class ReentrantLockTask extends LockTask {
    private static ReentrantLock locker = new ReentrantLock();

    public ReentrantLockTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        locker.lock();
        try {
            if (write) {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } else {
                hashMap.get(counter);
            }
        } finally {
            locker.unlock();
        }
    }
}
複製代碼

而後是ReentrantReadWriteLock,可重入的讀寫鎖,這屋裏咱們須要區分讀操做仍是寫操做來得到不一樣類型的鎖:微信

@Slf4j
class ReentrantReadWriteLockTask extends LockTask {
    private static ReentrantReadWriteLock locker = new ReentrantReadWriteLock();

    public ReentrantReadWriteLockTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        if (write) {
            locker.writeLock().lock();
            try {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } finally {
                locker.writeLock().unlock();
            }
        } else {
            locker.readLock().lock();
            try {
                hashMap.get(counter);
            } finally {
                locker.readLock().unlock();
            }
        }
    }
}
複製代碼

而後是可重入鎖和可重入讀寫鎖的公平版本:數據結構

@Slf4j
class FairReentrantLockTask extends LockTask {
    private static ReentrantLock locker = new ReentrantLock(true);

    public FairReentrantLockTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        locker.lock();
        try {
            if (write) {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } else {
                hashMap.get(counter);
            }
        } finally {
            locker.unlock();
        }
    }
}

@Slf4j
class FairReentrantReadWriteLockTask extends LockTask {
    private static ReentrantReadWriteLock locker = new ReentrantReadWriteLock(true);

    public FairReentrantReadWriteLockTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        if (write) {
            locker.writeLock().lock();
            try {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } finally {
                locker.writeLock().unlock();
            }
        } else {
            locker.readLock().lock();
            try {
                hashMap.get(counter);
            } finally {
                locker.readLock().unlock();
            }
        }
    }
}
複製代碼

最後是1.8推出的StampedLock:併發

@Slf4j
class StampedLockTask extends LockTask {
    private static StampedLock locker = new StampedLock();

    public StampedLockTask(Boolean write) {
        super(write);
    }

    @Override
    protected void doTask() {
        if (write) {
            long stamp = locker.writeLock();
            try {
                counter++;
                hashMap.put(counter, "Data" + counter);
            } finally {
                locker.unlockWrite(stamp);
            }
        } else {
            long stamp = locker.tryOptimisticRead();
            long value = counter;

            if (!locker.validate(stamp)) {
                stamp = locker.readLock();
                try {
                    value = counter;
                } finally {
                    locker.unlockRead(stamp);
                }
            }
            hashMap.get(value);
        }
    }
}
複製代碼

這裏一樣區分讀寫鎖,只是讀鎖咱們先嚐試進行樂觀讀,拿到一個戳後讀取咱們須要保護的數據,隨後校驗一下這個戳若是沒問題的話說明數據沒有改變,樂觀鎖生效,若是有問題升級爲悲觀鎖再讀取一次。由於StampedLock很複雜很容易用錯,真的打算用的話務必研讀官網的各類鎖升級的例子(樂觀讀到讀,樂觀讀到寫,讀到寫)。app

性能測試和分析

一樣咱們定義性能測試的類型:ide

@ToString
@RequiredArgsConstructor
class TestCase {
    final Class lockTaskClass;
    final int writerThreadCount;
    final int readerThreadCount;
    long duration;
}
複製代碼

每一種測試能夠靈活選擇:高併發

  • 測試的鎖類型
  • 寫線程數量
  • 讀線程數量
  • 最後測試結果回寫到duration

下面是性能測試的場景定義:

@Test
public void test() throws Exception {
    List<TestCase> testCases = new ArrayList<>();

    Arrays.asList(SyncTask.class,
            ReentrantLockTask.class,
            FairReentrantLockTask.class,
            ReentrantReadWriteLockTask.class,
            FairReentrantReadWriteLockTask.class,
            StampedLockTask.class
    ).forEach(syncTaskClass -> {
        testCases.add(new TestCase(syncTaskClass, 1, 0));
        testCases.add(new TestCase(syncTaskClass, 10, 0));
        testCases.add(new TestCase(syncTaskClass, 0, 1));
        testCases.add(new TestCase(syncTaskClass, 0, 10));

        testCases.add(new TestCase(syncTaskClass, 1, 1));
        testCases.add(new TestCase(syncTaskClass, 10, 10));
        testCases.add(new TestCase(syncTaskClass, 50, 50));
        testCases.add(new TestCase(syncTaskClass, 100, 100));
        testCases.add(new TestCase(syncTaskClass, 500, 500));
        testCases.add(new TestCase(syncTaskClass, 1000, 1000));

        testCases.add(new TestCase(syncTaskClass, 1, 10));
        testCases.add(new TestCase(syncTaskClass, 10, 100));
        testCases.add(new TestCase(syncTaskClass, 10, 200));
        testCases.add(new TestCase(syncTaskClass, 10, 500));
        testCases.add(new TestCase(syncTaskClass, 10, 1000));

        testCases.add(new TestCase(syncTaskClass, 10, 1));
        testCases.add(new TestCase(syncTaskClass, 100, 10));
        testCases.add(new TestCase(syncTaskClass, 200, 10));
        testCases.add(new TestCase(syncTaskClass, 500, 10));
        testCases.add(new TestCase(syncTaskClass, 1000, 10));

    });

    testCases.forEach(testCase -> {
        System.gc();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            benchmark(testCase);
        } catch (Exception e) {
            e.printStackTrace();
        }
    });

    StringBuilder stringBuilder = new StringBuilder();
    int index = 0;
    for (TestCase testCase : testCases) {
        if (index % 20 == 0)
            stringBuilder.append("\r\n");
        stringBuilder.append(testCase.duration);
        stringBuilder.append(",");
        index++;
    }
    System.out.println(stringBuilder.toString());
}
複製代碼

在這裏能夠看到,咱們爲這6個鎖定義了20種測試場景,覆蓋幾大類:

  • 只有讀的狀況
  • 只有寫的狀況
  • 讀寫併發的狀況,併發數漸漸增多
  • 讀比寫多的狀況(這個最多見吧)
  • 寫比讀多的狀況

每一次測試之間強制觸發gc後休眠1秒,每20次結果換行一次輸出。 測試類以下:

private void benchmark(TestCase testCase) throws Exception {
    LockTask.counter = 0;
    log.info("Start benchmark:{}", testCase);
    CountDownLatch start = new CountDownLatch(1);
    CountDownLatch finish = new CountDownLatch(testCase.readerThreadCount + testCase.writerThreadCount);
    if (testCase.readerThreadCount > 0) {
        LockTask readerTask = (LockTask) testCase.lockTaskClass.getDeclaredConstructor(Boolean.class).newInstance(false);
        readerTask.start = start;
        readerTask.finish = finish;
        readerTask.loopCount = LOOP_COUNT / testCase.readerThreadCount;
        if (testCase.lockTaskClass.getSimpleName().startsWith("Fair")) readerTask.loopCount /= 100;
        IntStream.rangeClosed(1, testCase.readerThreadCount)
                .mapToObj(__ -> new Thread(readerTask))
                .forEach(Thread::start);
    }
    if (testCase.writerThreadCount > 0) {
        LockTask writerTask = (LockTask) testCase.lockTaskClass.getDeclaredConstructor(Boolean.class).newInstance(true);
        writerTask.start = start;
        writerTask.finish = finish;
        writerTask.loopCount = LOOP_COUNT / testCase.writerThreadCount;
        if (testCase.lockTaskClass.getSimpleName().startsWith("Fair")) writerTask.loopCount /= 100;
        IntStream.rangeClosed(1, testCase.writerThreadCount)
                .mapToObj(__ -> new Thread(writerTask))
                .forEach(Thread::start);
    }

    start.countDown();
    long begin = System.currentTimeMillis();
    finish.await();
    if (testCase.writerThreadCount > 0) {
        if (testCase.lockTaskClass.getSimpleName().startsWith("Fair")) {
            Assert.assertEquals(LOOP_COUNT / 100, LockTask.counter);
        } else {
            Assert.assertEquals(LOOP_COUNT, LockTask.counter);
        }
    }
    testCase.duration = System.currentTimeMillis() - begin;
    log.info("Finish benchmark:{}", testCase);
}
複製代碼

代碼主要乾了幾件事情:

  • 根據測試用例的讀寫線程數,開啓必定量的線程,根據類名和讀寫類型動態建立類型
  • 每個線程執行的循環次數是按比例均勻分配的,公平類型的兩次測試數/100,由於實在是太慢了,等不了幾小時
  • 使用兩個CountDownLatch來控制全部線程開啓,等待全部線程完成,最後校驗一下counter的總數

在這裏,咱們把循環次數設置爲1000萬次,在阿里雲12核12G機器JDK8環境下運行獲得的結果以下:

image_1dg6a5bug4jpge6r181gdl7fl9.png-246.2kB

這裏,咱們進行兩次測試,其實一開始個人測試代碼裏沒有HashMap的讀寫操做,只有counter的讀寫操做(這個時候循環次數是1億次),全部第一次測試是僅僅只有counter的讀寫操做的,後一次測試是這裏貼的代碼的版本。

因此這個表格中的數據不能直接來對比由於混雜了三種循環次數,上面那個表是1億從循環的時間,下面那個是1000萬次,黃色的兩條分別是100萬次和10萬次循環。

這個測試信息量很大,這裏說一下我看到的幾個結論,或者你還能夠從這個測試中品味出其它結論:

  • synchronized關鍵字通過各類優化進行簡單鎖的操做性能已經至關好了,若是用不到ReentrantLock高級功能的話,使用synchronized不會有什麼太多性能問題
  • 在任務很是輕的時候可重入鎖比synchronized仍是快那麼一點,通常場景下不可能只是++操做,這個時候二者差很少
  • 併發上來以後各類鎖的執行耗時稍微增多點,沒有增多太厲害,併發不足的時候反而性能還很差
  • 在任務很輕的時候StampedLock性能碾壓羣雄,在只有讀操做的時候由於只是樂觀鎖,因此性能好的誇張
  • 在任務沒有那麼輕的時候讀寫鎖的性能幾乎都比普通鎖好,看下面那個表格,在任務實在是過輕的時候讀寫鎖由於複雜的鎖實現開銷的問題不如普通的可重入鎖
  • 公平版本的鎖很是很是慢,能夠說比非公平版本的慢100倍還不止,並且執行的時候CPU打滿,其它版本的鎖執行的時候CPU利用在12核的20%左右,其實想一想也對,不論是多少線程,大部分時候都阻塞了

因此說對於這些鎖的選擇也很明確:

  • 若是用不到ReentrantLock的什麼高級特性,synchronized就能夠
  • 通常而言ReentrantLock徹底能夠替代synchronized,若是你不嫌麻煩的話
  • ReentrantReadWriteLock用於相對比較複雜的任務的讀寫併發的狀況
  • StampedLock用於相對比較輕量級任務的高併發的狀況,用起來也比較複雜,可以實現極致的性能
  • 只有有特殊需求的話纔去開啓ReentrantLock或ReentrantReadWriteLock的公平特性

再來看看ReentrantLock

以前也提到了可重入鎖相對synchronized有一些高級特性,咱們寫一些測試代碼:

  • 咱們先在主線程鎖10次
  • 輸出一下鎖的一些信息
  • 循環10次開啓10個線程嘗試獲取鎖,等待時間是1秒到10秒,顯然主線程釋放鎖以前是獲取不到鎖的
  • 1秒一次定時輸出鎖的一些信息
  • 5秒後主線程釋放鎖
  • 休眠一下觀察子線程是否拿到鎖了
@Test
public void test() throws InterruptedException {

    ReentrantLock reentrantLock = new ReentrantLock(true);
    IntStream.rangeClosed(1, 10).forEach(i -> reentrantLock.lock());
    log.info("getHoldCount:{},isHeldByCurrentThread:{},isLocked:{}",
            reentrantLock.getHoldCount(),
            reentrantLock.isHeldByCurrentThread(),
            reentrantLock.isLocked());

    List<Thread> threads = IntStream.rangeClosed(1, 10).mapToObj(i -> new Thread(() -> {
        try {
            if (reentrantLock.tryLock(i, TimeUnit.SECONDS)) {
                try {
                    log.debug("Got lock");
                } finally {
                    reentrantLock.unlock();
                }
            } else {
                log.debug("Cannot get lock");
            }
        } catch (InterruptedException e) {
            log.debug("InterruptedException Cannot get lock");
            e.printStackTrace();
        }
    })).collect(Collectors.toList());

    Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> log.info("getHoldCount:{}, getQueueLength:{}, hasQueuedThreads:{}, waitThreads:{}",
            reentrantLock.getHoldCount(),
            reentrantLock.getQueueLength(),
            reentrantLock.hasQueuedThreads(),
            threads.stream().filter(reentrantLock::hasQueuedThread).count()), 0, 1, TimeUnit.SECONDS);

    threads.forEach(Thread::start);

    TimeUnit.SECONDS.sleep(5);
    IntStream.rangeClosed(1, 10).forEach(i -> reentrantLock.unlock());
    TimeUnit.SECONDS.sleep(1);
}
複製代碼

輸出以下:

08:14:50.834 [main] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:10,isHeldByCurrentThread:true,isLocked:true
08:14:50.849 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:10, hasQueuedThreads:true, waitThreads:10
08:14:51.849 [Thread-0] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:51.848 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:9, hasQueuedThreads:true, waitThreads:9
08:14:52.849 [Thread-1] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:52.849 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:8, hasQueuedThreads:true, waitThreads:8
08:14:53.846 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:8, hasQueuedThreads:true, waitThreads:8
08:14:53.847 [Thread-2] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:54.847 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:7, hasQueuedThreads:true, waitThreads:7
08:14:54.849 [Thread-3] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:55.847 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:6, hasQueuedThreads:true, waitThreads:6
08:14:55.850 [Thread-4] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:55.850 [Thread-5] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.851 [Thread-6] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.852 [Thread-7] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.852 [Thread-8] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.852 [Thread-9] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:56.849 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:0, hasQueuedThreads:false, waitThreads:0
複製代碼

從這個輸出能夠看到:

  • 一開始顯示鎖被主線程鎖了10次
  • 隨着時間的推移等待鎖的線程數量在增長
  • 5個線程由於超時沒法獲取到鎖
  • 5秒後還有5個線程拿到了鎖

這也能夠看到可重入鎖相比synchronized功能更強大點:

  • 能夠超時等待獲取鎖
  • 能夠查看到鎖的一些信息
  • 能夠中斷鎖(這裏沒有演示)
  • 以前提到的公平性
  • 可重入特性並非它特有的功能,synchronized也能重入

提到了可重入,咱們進行一個無聊的實驗看看能夠重入多少次:

@Test
public void test2() {
    ReentrantLock reentrantLock = new ReentrantLock(true);
    int i = 0;
    try {
        while (true) {
            reentrantLock.lock();
            i++;
        }
    } catch (Error error) {
        log.error("count:{}", i, error);
    }
}
複製代碼

結果以下:

image_1dg6c4snsrjr1179dnh19gu1qjem.png-167.6kB

鎖誤用的例子

最後再提下最簡單的鎖誤用的例子,雖然沒有那麼高大上,可是這種由於鎖範圍和鎖保護對象的範圍不一致致使誤用的問題在業務代碼中處處都是,好比:

@Slf4j
public class LockMisuse {

    @Test
    public void test1() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        IntStream.rangeClosed(1, 100000).forEach(i -> executorService.submit(new Container()::test));
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.HOURS);
        log.info("{}", Container.counter);
    }
}

class Container {
    static int counter = 0;
    Object locker = new Object();

    void test() {
        synchronized (locker) {
            counter++;
        }
    }
}
複製代碼

在代碼裏咱們要保護的資源是靜態的,可是鎖倒是對象級別的,不一樣的實例持有不一樣的鎖,徹底起不到保護做用:

image_1dg6d632bb3lkncdd31d6po3d13.png-53.5kB

小結

本文咱們簡單測試了一下各類鎖的性能,我感受這個測試可能還沒法100%模擬真實的場景,真實狀況下不只僅是讀寫線程數量的不一致,更可能是操做頻次的不一致,不過這個測試基本看到了咱們猜想的結果。在平常代碼開發過程當中,你們能夠根據實際功能和場景須要來選擇合適的鎖類型。

有的時候高大上的一些鎖由於使用複雜容易致使誤用、錯用、死鎖、活鎖等問題,我反而建議在沒有明顯問題的狀況下先從簡單的『悲觀』鎖開始使用。還有就是像最後的例子,使用鎖的話務必須要認證檢查代碼,思考鎖和保護對象的關係,避免鎖不產產生效果致使隱藏的Bug。

一樣,代碼見個人Github,歡迎clone後本身把玩,歡迎點贊。

歡迎關注個人微信公衆號:隨緣主人的園子

image_1dfvp8d55spm14t7erkr3mdbscf.png-45kB
相關文章
相關標籤/搜索