1、HashMap的底层结构:
① HashMap是基于Entry数组的链表结构(就是一个数组中,每一个元素都是一个链表,在插入元素的时候,首先通过对传入的键(key),进行一个哈希函数的处理,来确定该元素应该存放于数组中哪一个元素的链表中),在高并发的情况下,会造成链表闭环,死循环,线程不安全,线程安全可以使用HashTable(方法加synchronized)和ConcurrentHashMap(分段锁)。
② HashTable实现线程安全的代价比较大,那就是所有有可能产生竞争的方法里都加上了synchronized,这就导致在出现竞争时,只能一个线程对整个HashTable进行操作,其他线程都需要阻塞等待当前取到锁的线程执行完成,这样效率非常低。
③ ConcurrentHashMap则不同,当需要 put 元素的时候,并不是对整个 ConcurrentHashMap 进行加锁,而是先通过 hashcode 来判断它放在哪一个分段中,然后对该分段进行加锁。所以当多线程 put 的时候,只要不是放在同一个分段中,就可以实现并行的插入。分段锁的设计目的就是为了细化锁的粒度,从而提高并发能力(在jdk1.7版本是基于分段锁实现,在jdk1.8是基于CAS+synchronized实现)。
④ 从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,Segment继承于ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力能指向下一个节点的指针。
实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。
2、HashMap不是线程安全的,可能会发生这些问题:
① 多线程下扩容死循环。JDK1.7 中的 HashMap 使用头插法插入元素,头插法会使链表发生反转,在多线程的环境下,扩容的时候有可能导致链表闭环,形成死循环。因此,JDK1.8 使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
② 多线程的 put 可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在 JDK 1.7 和 JDK 1.8 中都存在。
③ put 和 get 并发时,可能导致 get 为 null。线程 1 执行 put 时,因为元素个数超出 threshold 而导致 rehash,线程 2 此时执行 get,有可能导致这个问题。这个问题在 JDK 1.7 和 JDK 1.8 中都存在。
3、HashMap的自动扩容:
① HashMap的默认数组大小是16,负载因子是0.75,但是实际储存大小是(16 * 0.75 = 12),当数组内元素超过12时,就会自动扩容16 * 2,如果还是不够就16 * 2 * 2以此类推。
② HashMap 的也有缺点,例如存储容量占比高,考虑到负载因子的存在,通常空间是不能被用满的,而一旦你的值很多例如上亿的时候,那 HashMap 占据的内存大小就变得很可观了。
③ 在Java1.8以后,当链表长度 >= 8,数组长度 >= 64 时会自动转换成红黑树(树化阈值)。
链表转化为红黑树需要满足2个条件:
④ 链表的节点数量(包括新增节点)大于等于树化阈值(查看源码可知,putVal方法是大于树化阈值,而其他方法是大于等于树化阈值)。
⑤ HashMap的容量(Node数组的长度)大于等于最小树化容量值。
4、红黑树本质上是一种二叉查找树,为了保持平衡,它又在二叉查找树的基础上增加了一些规则:
① 每个节点要么是红色,要么是黑色。
② 根节点永远是黑色的。
③ 所有的叶子节点都是是黑色的(注意这里说叶子节点其实是图中的 NULL 节点)。
④ 每个红色节点的两个子节点一定都是黑色。
⑤ 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
红黑树有两种方式保持平衡:旋转和染色