述
并发编程中,并发容器也是非常重要的一个工具,我们平时用的 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 打基础