HashMap/ConcurrentHashMap在单线程模式下的性能比较

起源

阅读源码发现jdk8中ConcurrentHashMap是基于synchronized来加锁实现多线程安全的,但是实现方式上与早期的HashTable又有了很大的区别,虽然都是使用synchronized来加锁,但是锁的粒度不一样,大致可以作如下理解:

  • HashTable JDK1.0版本开始提供,为了保证线程安全,内部使用了1把锁,读读、读写操作均会锁竞争
  • HashTable这种键值对的存储结构很多场景会用到,但是并发性能太差,大神Doug Lea(被评为“世界上对Java影响力最大的个人”)实在看不下去了,在JDK5中贡献了ConcurrentHashMap,既然HashTable一把锁并发冲突太高,那么多搞几把锁不就OK了吗,也就是所谓的“分段加锁”,强制将Hash数组拆分成多个区间,这样子当操作不同的“段”时就不会冲突了,拆分后大神Doug Lea没有采用synchronized(当时JDK5时synchronized性能本身也比较差)来给每个段加锁,而是采用的是ReentrantLock(使用的是CAS机制性能在当时会更好些)
  • 从JDK6开始synchronized借鉴了CAS思想,引入了偏向锁、轻量级锁、重量锁等概念,在后期的版本和一些硬件中synchronized性能已经与ReentrantLock持平,甚至更好些
  • JDK8中ConcurrentHashMap重新又使用synchronized关键字来加锁了,另外“分段加锁”理念已经过时,直接基于Hash数组中的每个元素进行加锁,这样子锁的粒度就更低了(比较好奇在JDK5时ConcurrentHashMa为什么不直接这么干)

介绍了这么多,现在我应该知道ConcurrentHashMap是线程安全的,而且优化了那么多个版本,然而它与非线程安全的HashMap读写性能那个更好呢

测试环境

本文中提及的代码,编译及运行过程均是在jdk8下操作的

mingyuedeMacBook-Pro:data mac$ java -version
java version "1.8.0_192"
Java(TM) SE Runtime Environment (build 1.8.0_192-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.192-b12, mixed mode)

测试对比

任何JVM参数均不使用

在这里插入图片描述

server模式下并堆大小(-server -Xms1G -Xmx1G)

说明:这种设置更加贴近生产环境,一般都会这么用

在这里插入图片描述

测试源码

有兴趣的小伙伴可以在电脑上试试,不用配置的电脑运行数据可能略有差异


package com.alioo.stresstest;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class HashMapDemo {

    static List<Integer> list = Arrays.asList(new Integer[]{1, 2, 3, 4, 5});

    //原始代码就是对HahMap/CurrenrtHashMap的探讨
    //结合原始代码,先存放,后遍历,这里假若存放的是5条记录 int [] arr={1,2,3,4,5}
    public static void test(Map<Integer, Future> futures) {

        list.forEach(i -> {
            Future f = new FutureTask(() -> {
                return null;
            });
            futures.put(i, f);
        });

        futures.forEach((k, v) -> {
            Integer i = k;
            Future f = v;
        });
    }


    public static long recodeCosttime(Map<Integer, Future> futures, int loopcount) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < loopcount; i++) {
            test(futures);
        }
        long end = System.currentTimeMillis();
        return (end - start);
    }

    public static void stresstest(int loopcount) {
        Map<Integer, Future> futures1 = new HashMap<>();
        Map<Integer, Future> futures2 = new ConcurrentHashMap<>();
        long costtime1 = recodeCosttime(futures1, loopcount);
        long costtime2 = recodeCosttime(futures2, loopcount);
        String line = String.format("%-20d \t %-20d \t %-20d", loopcount, costtime1, costtime2);
        System.out.println(line);
    }


    public static void main(String[] args) {
        String header = String.format("%-20s \t %-20s \t %-20s", "loopcount", "HashMap(ms)", "ConcurrentHashMap(ms)");
        System.out.println(header);

        stresstest(1000);//1000次
        stresstest(1000_000);//100d万次
        stresstest(1000_000_0);//1000万次
        stresstest(1000_000_00);//1亿次

    }

}


测试结论

  • 上述压力测试1000次,看到的 ConcurrentHashMap执行耗时更加短一些,这其实是一个错觉,主要是编译优化,代码预热的原因,可以通过交换上述的测试代码看出效果
long costtime2 = recodeCosttime(futures2, loopcount); //先执行这一行
long costtime1 = recodeCosttime(futures1, loopcount);
  • 压力测试1000w次时,ConcurrentHashMap均会比HashMap执行耗时多1s左右
  • 压力测试1亿次时,ConcurrentHashMap均会比HashMap执行耗时多10s左右

综上所述,ConcurrentHashMap尽管经过了那么多个版本的优化,但是当在单线程时HashMap性能一直领先地位(高能提示:这里加了修饰词“单线程时”哟,HashMap非线程安全的,所以在多线程下进行压测对比就没有意义了),所以你只需要按照以下套路操作写出来的代码就不会被人鄙视了

  1. 单线程时请使用HashMap
  2. 方法内部创建的Map结构请使用HashMap
  3. 需要保证线程安全时请使用ConcurrentHashMap,而不要使用HashTable
发布了100 篇原创文章 · 获赞 64 · 访问量 25万+

猜你喜欢

转载自blog.csdn.net/hl_java/article/details/99823770