ConcurrentHashMap性能测试

之前在测试commons-pool2相关实现的时候,发现在线程接近500时候,性能瓶颈降低非常厉害,就好像碰到了总体性能的天花板一样,随着线程继续增加而单线程性能急速下降的现象。当时粗略判断其中一个原因是用来存储对象映射关系的java.util.concurrent.ConcurrentHashMap存在瓶颈导致。

所以今天我特意来测试一下java.util.concurrent.ConcurrentHashMap的查询性能,其他增改的功能暂时不做测试了。关于另外一个可能的原因java.util.concurrent.atomic.AtomicLong,我们下期再测。有兴趣的可以先看看我之前对于更强大的多线程计数器java.util.concurrent.atomic.LongAdder的性能测试:性能测试中的LongAdder。下面是之前遇到两种不同类型的对象池的性能测试文章:通用池化框架GenericObjectPool性能测试通用池化框架GenericKeyedObjectPool性能测试

测试方案

先说一下思路和场景设计。思路还是沿用之前的性能测试,通过固定线程的性能模型进行测试,通过调整次数和线程数来测试java.util.concurrent.ConcurrentHashMap的性能表现。场景设计上我先把java.util.concurrent.ConcurrentHashMap添加N个keyvalue,然后通过多线程随机从这些key里面取值。

这样本地测试就有了三个变量线程数次数key的数量,本次重点放在了200线程以上的性能表现。

PS:硬件和软件配置参考以前的文章,这里就不多说了。

测试用例

照例方案依旧使用FunTester性能测试框架提供的能力,采取Groovy脚本实现。相信有一定Java基础的同学阅读起来是没有问题的。


package com.funtest.groovytest

import com.funtester.base.constaint.FixedThread
import com.funtester.base.constaint.ThreadBase
import com.funtester.frame.SourceCode
import com.funtester.frame.execute.Concurrent

import java.util.concurrent.ConcurrentHashMap

class ConcurrentHashMapTest extends SourceCode {

    static ConcurrentHashMap<Integer, Integer> maps = new ConcurrentHashMap<>()

    static int times = 1_0000

    static int threads = 200

    static int num = 100

    static def desc = "ConcurrentHashMap性能测试"

    public static void main(String[] args) {
        1.upto(num) {
            maps.put(it, it)
        }

        ThreadBase.COUNT = false
        RUNUP_TIME = 0
        new Concurrent(new FunTester(), threads, desc).start()
    }

    private static class FunTester extends FixedThread {


        FunTester() {
            super(null, times, true)
        }

        @Override
        protected void doing() throws Exception {
            maps.get(getRandomInt(num))
        }

        @Override
        FunTester clone() {
            return new FunTester()
        }
    }

}

测试结果

由于测试中基本都触碰到硬件(CPU)瓶颈,所以本次也就不记录CPU使用率了,相当于都是在CPU资源有限情况下的性能测试数据,其实测试中发现次数影响也不大。

线程数 次数(千) key数量 单线程QPS
200 10 100 3038
200 20 100 3539
200 40 100 4066
200 80 100 4334
200 10 200 2823
200 20 200 3587
200 40 200 4736
200 10 400 2919
200 10 50 2873
200 10 20 3218
200 10 1000 3256
300 10 100 1893
300 20 100 2514
300 40 100 3214
300 20 300 1798
300 20 500 2832
500 20 100 1722
500 20 1000 1509
1000 20 1000 816
1000 10 100 724

测试到此,结论比较明显了,影响java.util.concurrent.ConcurrentHashMap的主要因素还是机器CPU资源不够用了。对于相同的资源情况下,线程数更低自然获得更强的单线程性能,如果增加线程确实可以获取更大的总体QPS。在key值方面,值越多,QPS越低。在测试次数上,自然是字数越多,QPS也大,也符合之前多次测试中的结论。

但是当我重新检查代码的时候却发现一个问题,在com.funtest.groovytest.ConcurrentHashMapTest.FunTester#doing方法中其实还有一段耗时的请求,就是com.funtester.frame.SourceCode#getRandomInt,经过我重新测试,发现java.util.concurrent.ConcurrentHashMap的性能得到了十几倍的提升。

不得不说我大意了,本期文章标题应当修改为java.util.concurrent.ThreadLocalRandom性能测试。

一下是com.funtester.frame.SourceCode#getRandomInt的内容:

    /**
     * 获取随机数,获取1~num 的数字,包含 num
     *
     * @param num 随机数上限
     * @return 随机数
     */
    public static int getRandomInt(int num) {
        return ThreadLocalRandom.current().nextInt(num) + 1;
    }

我依此法重新测试了java.util.concurrent.atomic.AtomicLong,发现也是QPS超高,排除了我之前的想法。看来commons-pool2的瓶颈不在这两个地方。以后等我仔细再研究研究,有结论再跟大家分享。

Have Fun ~ Tester !

阅读原文,跳转我的仓库地址