HashMap 底层分析
HashMap 底层是基于数组和链表实现的
put()
将传入的 Key 做 hash 运算计算出 hashcode,然后根据数组长度取模计算出在数组中的 index 下标。若出现不同的 Key 通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在 table[index]处形成链表,采用头插法将数据插入到链表中。
get()
将传入的 Key 计算出 index ,如果该位置上是一个链表就需要遍历整个链表,通过 key.equals(k) 来找到对应的元素。
并发场景发生扩容,调用 resize() 方法里的 rehash() 时,容易出现环形链表。这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标时就会出现死循环。
并发状态下使用HashMap容易触发环型链表导致死循环,所以多线程下用CocurrentHashMap
参考资料:参考1 参考2
在 JDK1.8 中对 HashMap 进行了优化: 当 hash 碰撞之后写入链表的长度超过了阈值(默认为8),链表将会转换为红黑树。
ConcurrentHashMap 底层分析
ConcurrentHashMap 是由 Segment 数组、HashEntry 数组组成,HashEntry 和 HashMap 一样,仍然是数组加链表组成。
ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
CocurrentHashMap利用锁分段技术增加了锁的数目,从而使争夺同一把锁的线程的数目得到控制。
锁分段技术就是对数据集进行分段,每段竞争一把锁,不同数据段的数据不存在锁竞争,从而有效提高 高并发访问效率。
CocurrentHashMap在get方法是无需加锁的,因为用到的共享变量都采用volatile关键字修饰,保证共享变量在线程之间的可见性(每次读取都先同步缓存和内存,直接从内存中获取值),volatile为了让变量提供线程之间的内存可见性,会禁止程序执行结果的重排序(导致缓存优化的效果降低)
备注:volatile修饰的共享变量,当一个线程修改该变量时,会导致其他线程的工作内存中缓存变量的缓存行无效,其他线程要用该变量就不得不重新从内存中读取,保证了不会脏读
ConcurrentHashMap JDK1.8
放弃了Segment转而采用的是Node,抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。
ConcurrentHashMap 采用了数组+链表/红黑树的实现方式来设计,内部大量采用CAS操作,在链表的长度大于某个阈值的时候会将链表转换成红黑树进一步提高其查找性能。
put()
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入
get()
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 都不满足那就按照链表的方式遍历获取值。
Reference: 参考3