述
并发编程中,并发容器也是非常重要的一个工具,我们平时用的 ArrayList
, HashMap
等等这些集合类都不是线程安全的,在并发环境下就可能会出现问题,JDK给我们提供了一些线程安全的容器,供我们使用
过时的并发容器
在早期的版本中, Hashtable
和 Vector
就对应的是线程安全的 HashMap
和 ArrayList
这两个线程安全类,本质上就是给方法加上 synchronized
锁,把方法都变成同步方法,这种方式的性能是很差的
后来又可以使用 Collections.synchronizedList(new ArrayList<>());
, Collections.synchronizedMap(new HashMap<K, V>());
这种方法把一个普通集合转成并发容器,这种方式,这种方式的原理其实也是用 synchronized
锁,区别是这种方式是锁的一部分代码块,上面的是锁的整个方法, 性能上有些许提升,但也还是很差
所以后来就出现了 ConcurrentHashMap
和 CopyOnWriteArrayList
这样的性能较为优秀的并发容器,这两个也是本文的重点
为什么HashMap不是线程安全的
- 同时put碰撞导致数据丢失,
HashMap
使用的是链表的数据结构, 如果多个线程同时put操作,两个key的hash值相同的话,他们两个会落到同一个地方,最终结果肯定是只有一个线程put成功了,另一个的数据就丢失了 - 扩容导致数据丢失,多个线程往里面put数据,当容量不够用的时候,多个线程同时去扩容,最终也是只有一个线程扩容成功,那么另一个线程的数据就会丢失
- 可能会死循环造成CPU100%, 具体原因可以参考这里
HashMap数据结构
在了解 ConcurrentHashMap
之前,首先需要了解一下 HashMap
HashMap
底层是通过数组+链表组成的,在 jdk1.7和jdk1.8中的实现有些区别
JDK1.7中的 HashMap
jdk1.7 中 HashMap
的数据结构是这样的
这里就是一个数组+链表的结构,进入1.7中 HashMap
的源码看一下
这里是 HashMap
的几个重点的属性,这里重点看一下负载因子, HashMap
中默认的容量是16,负载因子是0.75,Map在使用的时候不断往里放数据,当容量达到了 16 * 0.75 = 12,就需要将当前的16的容量进行扩容,而扩容这个过程涉及到了 rehash
,复制数据等操作,非常耗费性能,所以阿里巴巴开发规范中,推荐尽可能在Map初始化的时候就给定容量
然后这里具体存放数据的就是 Entry
看一下他的源码
这里的 next 就是用于实现链表的数据结构
知道了基本的数据结构后,再来看一下里面的几个重要方法
首先是 put
方法,代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public V put(K key, V value) {
// 如果 key 为空,则 put 一个空值进去
if (key == null)
return putForNullKey(value);
// 根据 key 计算出 hashcode
int hash = hash(key.hashCode());
// 根据计算出的 hashcode 定位出所在桶
int i = indexFor(hash, table.length);
// 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果桶是空的,说明当前位置没有数据存入,新增一个 Entry 对象写入当前位置
modCount++;
addEntry(hash, key, value, i);
return null;
}
这里再看一下 addEntry
这个方法
1 | void addEntry(int hash, K key, V value, int bucketIndex) { |
再来看看 get()
方法
1 | public V get(Object key) { |
到这里 1.7 的 HashMap
就介绍完了,这个数据结构有一个需要优化的地方,就是当 hash 冲突比较严重的时候,链表就会越来越长,这样查询的效率也会越来越低的
所以再来看看 1.8 中的 HashMap
是如何处理的
JDK1.8中的 HashMap
首先看一下数据结构,如图
相对于1.7的改变就是加了红黑树,当链表太长,达到设置的阈值的时候,就会将整个链表转成红黑树
下面来看看1.8中的几个重点方法,首先是 put()
方法,代码如下
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, |
各种赋值比较可能有点绕,多看几次, 下面再看看 get()
方法
1 | public V get(Object key) { |
1.8 中转为红黑树的数据结构,提高了查询效率,但是在并发环境下的问题还是存在的,因此就诞生了 ConcurrentHashMap
专用于并发环境下的 HashMap
总结
本文主要了解 HashMap
的数据结构,以及一个进化的过程,为了解 ConcurrentHashMap
打基础