- HashMap
- HashMap是基于hash表的一种数据结构,用于存放键值对,核心就是把hash值映射到数组的索引位,通过数组+链表(JDK1.8开始通过数组+链表+红黑树)解决Hash冲突。
- 因为当hash冲突较多时,链表中元素增加,查询插入等操作的时间复杂度从O(1)——>O(n)
- 而二叉树的时间复杂度O(log n)
- 数组下标=(数组长度-1)&hash(key),不同的key可能会产生相同的hash值,从而导致hash冲突,jdk1.7之前链表采用头插法,但是多线程环境下可能导致环路从而引发死循环。Jdk1.8之后采用尾插法。
- HashMap初始容量16,负载因子0.75,那么阈值就是16*0.75=12,超过12就会触发2倍扩容操作
- 扩容时,每个元素存储位置将根据新的数组容量重新计算hash值,并移动到新的数组中,这个操作加rehashing;
- Jdk1.7之前,就是存粹的把所有元素重新hash,然后一个一个搬过去
- Jdk1.8之后,旧数组长度16即010000,新数组长度32即100000
- 而数组下标=(数组长度-1)&hash(key),16-1为001111,32-1为011111
- 所以重点就在于key的hash值从右往左第五位是否为1,如果为1那么久需要搬迁,新数组下标=原下标+旧数组长度16。
- 如果是0的话,怎么与都是0,所以新数组高位按位与操作不受影响(注意:与操作有0为0,全1为1),那就说明在原位置,不需要迁移。减少了一部分hash计算的开销。
- 从jdk1.8开始,为了优化hash冲突时的查询性能,
- 当链表的数量大于等于8并且数组的大小大于等于64,链表就会转换成红黑树。
- 当数组长度小于64,则选择扩容。
- 当数组容量较小,过早的转换成红黑树,可能很快又会触发数组扩容,导致没必要过早树化的开销。
- 另外红黑树比链表更占内存。
- 树的元素低于6的时候,树就会转换成链表。
- 常见的处理hash冲突的办法:拉链法,开放寻址法,再哈希法
- HashMap VS Hashtable
- 线程安全
- HashMap 线程不安全,多线程环境下会产生数据一致性问题。
- Hashtable 线程安全,内部的方法采用了synchronized,保证了多线程并发的安全性。
- 性能差异
- HashMap 由于没有线程同步的开销,所以性能优于Hashtable。
- Hashtable 由于方法的锁机制,性能不如HashMap。
- 空值null
- HashMap 允许一个null键,多个null值。
- Hashtable 键值都不允许null,会报空指针异常。
- ConcurrentHashMap
- ConcurrentHashMap是Hashtable的替代方案,读操作的无锁化,写操作的分段锁机制提高了并发的性能,避免了全局锁的瓶颈。性能优于Hashtable。
- JDK1.7 ConcurrentHashMap
- 采用的是分段锁,每个Segment是独立的。默认16个,所以最多支持16个并发。
- 采用数组+链表
- 原理 :
- 先通过key的hash判断应该在哪个Segment的数据下标,然后对这个Segment加锁(Segment继承ReentrantLock,自带加锁的能力)。
- 然后再次通过key的hash得到Segment里HashEntry的数组下标,接下去的步骤同HashMap。
- 扩容:
- 扩容时针对单个Segment进行扩容的,不会影响其他的Segment,每个Segment都有自己的负载因子。因此它并不是针对整个ConcurrentHashMap扩容的。
- Size大小:先尝试不加锁三次计算sum值,如果三次值相同,就直接返回;如果不同就对每个Segment进行加锁计算
- 缺陷:Segment数组一旦初始化了之后,就不会扩容,导致对并发度控制过于死板(只有HashEntry才会扩容)
- JDK1.8 ConcurrentHashMap
- 移除了Segement的概念,锁的粒度更加细化,通过CAS进行写入操作,只有在更新链表和红黑树的节点时才会用到synchronized,并且只锁链表或树的头节点(相当于Node数组的每个节点都能上一把锁,并发度更高了)。
- 数组+链表+红黑树
- 原理:
- 先计算key的hash后得到数组下标,如果数组下标没有Node,就通过CAS塞入新的Node。
- 如果当前数组下标存在Node,那么则先通过Synchronized对这个Node加锁。
- 然后再判断key是否相等,key相同就替换value, 反之就Hash冲突(操作同HashMap)
- 扩容:
- Jdk1.8 取消了Segment,因此整个ConcurrentHashMap数组都会被扩容。通过CAS操作确保线程的安全,避免锁住整个数组,并且扩容时多个线程共同参与,逐步迁移就数据到新数组中。
- 在putVal()的时候,如果无限循环的内部正在扩容,就调用helpTransfer()帮忙一起扩容,扩容完毕后还会向新的数组中插入元素的。
- Size大小:jdk1.8中,搞了一个数组CounterCell ,每次put和remove的时候,先通过CAS修改baseCount的值,如果CAS失败,则说明存在线程竞争,就要通过hash到对应的CounterCell数组的下标下维护对应的数量;最终size=baseCount+所有的CounterCell
- ConcurrentHashMap volicate关键字保证了可见性,使得get的时候不用加锁也能线程安全。
- ConcurrentHashMap key 和value都不支持null。