HashMap
HashMap
- 1.HashMap的数据结构,在jdk8之前使用数组+链表,叫做链地址法,jdk8之后使用的是数组+链表+红黑树,数组容易查询难修改,链表慢查询容易修改,所以两者相结合
- 2.计算完key哈希值之后,要根据key计算出应该对应的数组下标,这个时候就需要对数组的长度进行取模运算,这里采用的是位运算按位与,因为位运算是直接操作内存数据,不需要从十进制转换成二进制,速度更快一些x%2^n==x&(2 ^n -1),所以HashMap的长度都是2的幂
- **3.HashMap中几个关键的参数:size()表示插入键值对的个数,capacity表示table(也就是存储链表和红黑树节点的数组)的长度初始默认为16,负载因子loadFactor默认0.75(经过数学推算这个负载因子在0.7左右能在时间和空间上做很好的权衡,因为长度是2的幂,所以乘以0.75正好也是一个整数,并且不会有过多的空余位置浪费),threshold记录了扩容的阈值==capacity*loadfactor **
- 4.HashMap是如何保证capacity是2的幂呢,第一在初始化的时候如果有设置的大小,会通过位运算转换成一个指定值大的第一个2的幂,第二个就是扩容机制,当size数量超过threshold阈值的时候就会进行扩容
- 5.在阿里巴巴开发手册上强调过集合类初始化必须指定集合大小,HashMap指定大小最好是 期待空间大小值/负载因子+1这么大,能够有效减少扩容的机制
- 6.扩容容量是原来的2倍,如果桶节点没有形成链表就直接rehash,如果有链表,就要对节点进行重新链接,如果链表已经形成了树,就取消树化。扩容之后的节点位置要不然就是原位置,要不然就是原索引+原容量,无需重新计算hash值
- 7.计算hash值的时候采用hash扰动,如果两个key二进制高位不同但是低位相同就很容易产生hash冲突,所以hash扰动就是拔高位置信息混入到低位值,目前jdk8是把计算的hashCode右移16位
- 8.引入红黑树原因就是如果hash冲突太多,导致某一个桶(bucket)的链表过长,那么查询或者put就无限接近于O(N)复杂度。所以就想使用二叉树,因为left<root<right查询的时间复杂度是logN ,但是如果极端情况一条边特别长又会退化到O(N),所以我们就想到使用二叉平衡树,使得左右两边的高度差维持在1,但是这样的要求又太严格了,要通过不断地左旋或者右旋来维持,又会增加插入元素的时间消耗。最后我们使用了红黑树,他的叶子节点都是黑色的不存储数据,相邻的两个节点不能都是红色的,每一个节点到达叶子结点所包含的黑色节点数是相同的,根节点必须是黑色的。他的插入最多旋转两次,删除最多旋转三次,所以时间复杂度是要优于这个平衡二叉树的
- 9.为什么不在发生hash冲突的时候就转换成红黑树呢,要等到链表长度为8才进行转换:红黑树的空间是链表的两倍,立即转换之后浪费空间,红黑树虽然查询比链表快,但是插入的时候要进行旋转和变色,所以如果链表长度小于8就转换从时间和空间上来说效果并不好。而且通过数学计算因为hash冲突导致链表长度达到8这个数字概率是很小的,所以官方才指定在8的时候才转换成红黑树。然后在节点数小于6的时候又退化成链表,因为不能一小于8就立马转换回去,这样转换太频繁了
- 10.首先是在LinkedHashMap中使用了双向链表维护插入顺序,然后HashMap 的普通链表节点Node是没有pre指针的,是树节点TreeNode继承了LinkedHashMap.Entry从而有了pre/next双向链表,避免在Node节点中添加pre产生浪费,java的设计思想“复用优先,按需扩展”,通过双向链表就可以在树退化的时候快速转换成链表,无需重新构建节点之间的关系,包括删除某个中间节点也可以直接用过pre指针找到上一个节点,重新成链
- 11.如果hashMap元素key没有实现comparable结构拥有比较能力,那么红黑树会使用仲裁方法进行哈希值比较和系统身份哈希值比较
- 12.在jdk7之前,扩容的时候通常采用的是头插法,当时认为后插入的数据使用的概率更高,成为热点节点,并且不需要遍历一边到尾部,修改next指针变成新的根节点,操作简单只需要O(1)的时间复杂度,但是尾插法就需要遍历到尾部,需要O(n)的时间复杂度,但是如果两个线程同时操作这个链表,就有可能导致节点的next指针互相指向形成环,后续再遍历节点的时候就有可能陷入死循环导致CPU飙升。 所以在jdk8之后改成了尾插法。还存在其他问题比如多个线程put的时候size个数不一样,多个put可能会产生数据覆盖,当既有get又有扩容的时候可能换了桶get不到了
- 13.hash冲突如何解决:链地址法,开放定址法(在hash数组中寻找空闲的位置来存储,线性探测一个一个往下找空闲的位置,二次探测按照平方来找),再哈希法
- jdk7和jdk8有那些不一样:数据结构引入红黑树,插入方式变成尾插法,扩容机制从全量rehash变成看新增的那个bit位是1还是0,hash的计算:变成了一次位运算+一次异或
ConcurrentHashMap
-
ConcurrentHashMap是如何保证线程安全的:在jdk8之前采用分段锁,把数组分成多个段,给每个段加上锁,但是每个段就是一个小的hash表并且要预分配独立的锁对象,要独立占用额外内存,实际上就使用空间换并发能力。而且这个段数固定在高并发的时候很有可能出现热点段,成为性能瓶颈
-
在jdk8之后使用CAS+分段锁,1.更细的锁力度,对节点加锁而不是对段,降低了锁的竞争。2.无锁CAS操作提高效率,无锁优先减少了加锁的场景。3.锁数量的减少也减少了内存开销
-
为什么加锁不使用ReentrantLock要使用synchronized:1.在对节点进行加锁的情况下,多个线程同时竞争同一个节点锁的概率没那么大,所以并发冲突没那么大,不会频繁升级为重量级锁,偏向锁和轻量级锁就可以搞定,所以在性能方面其实是和ReentrantLock差不多的。2.但是synchronized不需要手动的加锁释放锁,而且他是JVM的内置语义,能做出相应的优化操作比如锁细化,锁消除。3.当获取锁失败的时候synchronized会自旋避免被挂起,但是ReentrantLock会导致线程挂起,不挂起就能减少线程上下文切换的开销,并且不挂起就不需要唤醒。4.ReentrantLock是一个独立的对象,除了有锁的状态信息还有其他的控制并发访问的信息,所以内存开销会大一些
-
ConcurrentHashMap的值不支持Null,是为了避免二义性,因为在我们用get(key)获得一个Null值我们不确定是这个value值是空还是没有找到这个key返回空,如果是HashMap我们可以通过Contains(key)判断这个key是否存在,但是在并发的情况下就没办法判断,因为可能会有其他线程进行修改
-
ConcurrentHashMap实现了fail-safe机制,这是一种处理并发修改的策略,当集合遍历的时候允许其他线程进行修改且不会抛出异常,实现原理:1.对当前的数据进行快照,对这个快照进行遍历,2.不依赖于modCount的检测,所以在被修改的时候不会抛出异常,3.弱一致性的实现,迭代器看不到创建之后的所有修改,但是可以看到创建之前的所有修改。 放弃了强一致性换取了高并发修改的能力,避免迭代时候对整个容器加锁导致性能问题
-
ConcurrentHashMap在哪些地方实现了并发控制:一个重要参数sizeCtl是全局控制器,通过一个变量的不同取值协调初始化,扩容,并发操作。当触发扩容的时候sizeCtl就被更新成-1,有一个线程再-1,比如-3就表示有两个线程,初始化过程去看雀语内容。ConcurrentHashMap分别在初始化桶,进行put操作以及初始化的时候控制了并发

浙公网安备 33010602011771号