面试题之 浅谈HashMap 和 ConcurrentHashMap原理

HashMap
1、hashMap结构问题 1.7 和1.8
HashMap1.7 数组+ 单链表
HashMap1.8 数组+单链表+红黑树
在1.8的情况下,什么时候链表会转变成红黑树–>
当 链表长度大于8, 并且数组容量超过64,那么此时链表就会转换成红黑树
在1.8的情况下 ,什么时候红黑树会退化成一个单链表
当红黑树得节点如果说小于6,此时就会把红黑树重新转变成单链表
2、hashmap的哈希冲突问题
当我们插入数据时,希望让不同的元素能够落到不同得位置上,因为如果落到相同得位置上,就会产生哈希冲突,而一旦产生哈希冲突,就会形成链表挂上去,而一旦挂上去之后,链表得查询性能其实挺低得,所以hashmap 就要设计一个算法来避免这个事, 这个算法就是 hash算法 或者 叫做寻址算法 .
他的计算公式就是 利用插入元素的key算出hashcode ,把这个结果向右移动16位, 再把两个结果取 异或 . 再把这个结果 & 上 数组长度(2的n次幂 ) 减1 , 最后得到数组的索引 , 这个式子最终的效果就是要让 参与运算的位数越多,不一样的概率就越大.
((key.hashCode= h) ^ (h>>>16) ) & 数组长度(2的N次幂) -1 = 数组得索引
式子一意义:
1、做到了让32位的二进制尽可能的参与到运算中来
2、他在高低计算中采用,而这个符号他是所有二进制计算中,得到0,1 平均概率最高的一个符号
式子二意义:
1、&是所有二进制计算中,运算速度最快的一个二进制,因为他获得操作系统底层的支持
2、讨论一下奇偶数这个事,末位位是奇数,那么最终就一定是个奇数
在我们寻址算法中,如果式子二是一个偶数了(最后一位数为0),根据 & 运算(& 同 1 得 1 , 其他为 0 ), 当前的数组索引算出来 一定是个 偶数 ,所以式子二只能是个奇数,而偶数-1 那么他就一定是个奇数了
3、你说如果 式子一,好不容易算出来一个 尽可能位数参与到运算中来,并且还是0,1 尽可能的平均的式子,然后式子二告诉我,哼,我不用,也就是说采用偶数-1 ,得到这种类似于011111的式子可以尽可能的还算式子一的意义
4、采用&和 数组长度-1 可以保证 当前算出来的索引 得到的结果 是在 0 ~ 数组长度-1 的这个范围中,也就不存在越界的
3、hashMap扩容问题 1.7 1.8
首先map中有个loadFactor(扩容因子),默认值是0.75, 如果此时检测map中元素的个数大于阈值(0.75 * 16 = 12)之后,就会触发扩容 , 他就会扩容成之前数组长度的两倍.
而jdk1.7 和 1.8 的扩容原理有一些区别
1.7 ,其实就是双层遍历, 把原数组中的每一个元素重新拿出来,再走一次寻址算法 , 找到在新数组中的位置
1.8 也是双层循环 拿到这个节点, 然后 他会先去判断这个oldCap(扩容前的容量) && 上e.hash(之前算出来的hash值) 的结果是否等于 0 , 如果等于 0 , 就不用算了,在新数组中的位置 和 是老数组中的位置是不变的 , 如果不等于0 , 那他就会移动 , 会移动 oldCap.length这么多位
4、hashMap线程安全问题 通过hashMap 引导这个面试走向多线程
hashmap 他不是线程安全的 , 在多线程情况下,
jdk1.7时使用的头插法,在插入时扩容的情况下,可能会形成环形链表, 就会出现死循环问题,整个程序就永远都结束不了 , 还有就是在插入操作的时候,可能会有丢数据的问题.
而在1.8 时, 改成了尾插法 ,1.7 的死循环问题得到了解决, 但是依然存在丢数据问题.
所以hashmap他不是线程安全的 , 所以我们在开发时候,在多线程下,我们不会使用hashMap , 可选方案是使用: -
hashTable 他是方法上加了大量的synchronized锁,像synchronized这样得重量级得锁,就会出现性能问题. 还有一个 Collections.synchronized 也能让集合线程安全,但是性能都太低了,都是去加大量得sync 锁而已. 所以使用这个conucrrentHashMap

如果向hashMap 插入相同的元素,会怎么样呢?
会key不变,value覆盖
HashMap和hashTable区别
1、hashMap线程不安全,hashTable是线程安全的
2、hashMap是jdk1.2出现的,hashTable是jdk1.0出现的
3、hashMap能处理null值和null键,hashTable 不能处理null值和null键
为什么1.7要使用头插法:
我个人认为hashMap的作者是这么思考的:后来的元素大概率是先会被使用到的,所以1.7采用了头插

LinkedList 增删快 , 查询慢 . ArrayList 查询快 , 增删慢
当我们插入大量数据时, 应采用 ArrayList , 因为都需要扩容 , 速度慢主要是集中在扩容上了, 而ArrayList到后面其实并不需要频繁的扩容

concurrentHashMap

在JDK1.7中,ConcurrentHashMap 是基于 Segment+HashEntry数组实现的。Segment继承了ReentrantLock , 他采用了分段式加锁,这个分段式加锁的核心逻辑,相较于原来加上了一个整体锁,这个做法就高效很多,就是把map拆分成若干个段,每个段就加每个段的锁,各自管各自的, 实际上本身也不会有线程安全问题,只有大家操作同一个段才会有问题

在1.8 中 , 摒弃掉了segment,采用无锁化机制:synchronized+CAS+红黑树 这种方式来实现的, 锁的粒度也从段锁缩小为结点锁 (引入了红黑树结构用于 降低哈希冲突严重的场景的时间复杂度。)

在1.8 时 分析原来的map的哪些地方有线程安全问题
1、初始化(一个线程初始化好,被其他线程跑过来覆盖了,重新给初始化了)
会有一个while 循环,一直让你多个线程同时来初始化, 在while循环中 会判断当前这个哈希表是否初始化好了,如果没有初始化好,会让多个线程去进行cas 抢锁, 抢到锁得线程就去独自进行初始化,其他没有抢到锁得线程,由于a线程还没初始化好,所以其他线程其实还处于while循环中,但是只有a线程进去,其他线程其实都会满足那个if条件,满足if条件 进行线程礼让 , 目的是减少自旋, 之所以让他们在一个while循环去不停的 礼让,不停的去抢锁,是因为害怕 持有锁的线程初始化失败。

2、写数据 (会有数据覆盖问题)
此时如果初始化好了, 根据寻址算法 算出数组索引 , 如果当前这个索引上没有数据 , 就直接通过cas写入数据. 如果这个索引上有数据 , 就会进入一个 syn锁 , 而且这个syn锁只会锁住当前数组位置 及挂在这个位置上的单链表 或者 红黑树 , 然后写入数据 , 保证线程安全.

3、扩容(重复扩容 ,覆盖数据)
核心思路:
ConcurrentHashMap 他会把这个map拆分成若干个段,然后多个线程他们就去找 concurrentHashMap领取任务,concurrentHashMap 会根据这多个线程Ncpu性能来去给当前线程分配 扩容任务 (步伐),最后当所有领取到任务的线程都把他们的扩容任务处理好之后,然后这个扩容就OK

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值