说起HashMap和ConcurrentHashMap,我们很多人都会知道HashMap是线程不安全的,ConcurrentHashMap是线程安全的;
看下面的代码示例:
public static void main(String[] args) {
// Map<String,String> map = new ConcurrentHashMap<>();
Map<String,String> map = new HashMap<>();
int n=100;
CountDownLatch cdl = new CountDownLatch(n);
for(int i=0; i<n; i++) {
new Thread(()->{
for(int j=0;j<100;j++) {
String key = UUID.randomUUID().toString();
map.put(key, key);
}
cdl.countDown();
}).start();
}
try {
cdl.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(map.size());
}
代码中用了100个线程,每个线程put100次,结果会发现,使用ConcurrentHashMap时,结果就是预期的10000,但是使用HashMap时,会发现结果总是小于10000,说明它丢失了部分数据;
HashMap线程不安全具体是哪里的不安全呢?而ConcurrentHashMap又是怎么保证线程安全的?
看过源码的人应该都知道,HashMap内部数据结构就是一个数组+链表(jdk1.8后链表长度超过阈值后会转为红黑树)的结构;
HashMap的put全过程如下图所示(为了画图方便,不完全等同于源码逻辑):
整个put过程中,有很多都是线程不安全的行为,简单列举两点如下:
1、初始化数组不安全,多线程环境中B线程可能覆盖掉A线程
2、添加元素时不安全
同理,在链表上添加节点或者树上添加节点时以及数组扩容等,都会存在线程不安全问题。
ConcurrentHashMap的put过程类似于HashMap,那它又是如何确保上诉这些环节的线程安全呢?
如下面简略图,关键节点处都使用到了锁,有的是CAS乐观锁,有的是synchronized锁,使用synchronized锁时,锁对象就是当前数组下标位置的第一个元素,所以数组length有多大,就可以支持多大的并发写。