这个专题我发现怎么慢慢演化为性能测试了,遇到任何东西我就忍不住去测一把。本文咱们会大概看一下各类锁数据结构的简单用法,顺便也会来比拼一下性能。java
首先,咱们定一个抽象基类,用于各类锁测试的一些公共代码:git
@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中释放锁:微信
@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:app
@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很复杂很容易用错,真的打算用的话务必研读官网的各类锁升级的例子(乐观读到读,乐观读到写,读到写)。ide
一样咱们定义性能测试的类型:高并发
@ToString @RequiredArgsConstructor class TestCase { final Class lockTaskClass; final int writerThreadCount; final int readerThreadCount; long duration; }
每一种测试能够灵活选择:oop
下面是性能测试的场景定义:
@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); }
代码主要干了几件事情:
在这里,咱们把循环次数设置为1000万次,在阿里云12核12G机器JDK8环境下运行获得的结果以下:
这里,咱们进行两次测试,其实一开始个人测试代码里没有HashMap的读写操做,只有counter的读写操做(这个时候循环次数是1亿次),全部第一次测试是仅仅只有counter的读写操做的,后一次测试是这里贴的代码的版本。
因此这个表格中的数据不能直接来对比由于混杂了三种循环次数,上面那个表是1亿从循环的时间,下面那个是1000万次,黄色的两条分别是100万次和10万次循环。
这个测试信息量很大,这里说一下我看到的几个结论,或者你还能够从这个测试中品味出其它结论:
因此说对于这些锁的选择也很明确:
以前也提到了可重入锁相对synchronized有一些高级特性,咱们写一些测试代码:
@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
从这个输出能够看到:
这也能够看到可重入锁相比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); } }
结果以下:
最后再提下最简单的锁误用的例子,虽然没有那么高大上,可是这种由于锁范围和锁保护对象的范围不一致致使误用的问题在业务代码中处处都是,好比:
@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++; } } }
在代码里咱们要保护的资源是静态的,可是锁倒是对象级别的,不一样的实例持有不一样的锁,彻底起不到保护做用:
本文咱们简单测试了一下各类锁的性能,我感受这个测试可能还没法100%模拟真实的场景,真实状况下不只仅是读写线程数量的不一致,更可能是操做频次的不一致,不过这个测试基本看到了咱们猜想的结果。在平常代码开发过程当中,你们能够根据实际功能和场景须要来选择合适的锁类型。
有的时候高大上的一些锁由于使用复杂容易致使误用、错用、死锁、活锁等问题,我反而建议在没有明显问题的状况下先从简单的『悲观』锁开始使用。还有就是像最后的例子,使用锁的话务必须要认证检查代码,思考锁和保护对象的关系,避免锁不产产生效果致使隐藏的Bug。
一样,代码见个人Github,欢迎clone后本身把玩,欢迎点赞。
欢迎关注个人微信公众号:随缘主人的园子