起源
阅读源码发现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非线程安全的,所以在多线程下进行压测对比就没有意义了),所以你只需要按照以下套路操作写出来的代码就不会被人鄙视了
- 单线程时请使用
HashMap
- 方法内部创建的Map结构请使用
HashMap
- 需要保证线程安全时请使用
ConcurrentHashMap
,而不要使用HashTable