hashMap

  • HashTable 和 ConcurrentHashMap 的 共同特点 是 不允许 key 和 value 为 null , 注意它们的 键值 都 不能是 null 呀 ,而 HashMap 就比较随意,都可以是 null。

hashmap1.7 扩容

 void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {  //如果size大于threshold && table在下标为index的地方已经有entry了
            resize(2 * table.length);       //扩容,将数组长度变为原来两倍
            hash = (null != key) ? hash(key) : 0;       //重新计算 hash 值
            bucketIndex = indexFor(hash, table.length); //重新计算下标
        }

        createEntry(hash, key, value, bucketIndex);     //创建entry
    }

当put的元素个数大于12时,即大于hashMap的容量*负载因子计算后的值,那么就会进行扩容,上述源代码可以看到扩容的条件, 除了大于12,还要看当前put进table所处的位置,是否为null,若是null,就不进行扩容,否则就扩容成原来容量的2倍,扩容后需要重新计算hash和计算下标,由于table的长度发生了变化,需要重新计算。

hashMap 1.8

hashMap和1.7最大的区别就是引入了红黑树

put的时候加入了红黑树,当put元素时,若链表的长度大于8,即源代码中的TREEIFY_THRESHOLD的值,这个时候链表就会转化为红黑树结构;当进行扩容的时候,红黑树转移后,若元素个数小于6,那么就会重新转化为链表。

 

JDK1.7中的ConcurrentHashMap

JDK1.7中的ConcurrentHashMap和JDK1.7中的HashMap的区别就是数组所存的元素,我们知道ConcurrentHashMap 是线程安全的。

看到1.7中的ConcurrentHashMap数组中所存的是segments,每个segments下都是一个hashTable。当put元素时,会加锁,然后计算hash和下标,计算下标会计算两次,一次是在数组中的segments的位置,一次是在hashTable的位置。

二、哈希表如何解决Hash冲突?

三、为什么HashMap具备下述特点:键-值(key-value)都允许为空、线程不安全、不保证有序、存储位置随时间变化

四、为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键

五、HashMap 中的 key若 Object类型, 则需实现哪些方法?

(1)为什么在JDK1.7的时候是先进行扩容后进行插入,而在JDK1.8的时候则是先插入后进行扩容的呢?

//其实就是当这个Map中实际插入的键值对的值的大小如果大于这个默认的阈值的时候(初始是16*0.75=12)的时候才会触发扩容,
//这个是在JDK1.8中的先插入后扩容
if (++size > threshold)
            resize();
复制代码

其实这个问题也是JDK8对HashMap中,主要是因为对链表转为红黑树进行的优化,因为你插入这个节点的时候有可能是普通链表节点,也有可能是红黑树节点,但是为什么1.8之后HashMap变为先插入后扩容的原因,我也有点不是很理解?欢迎来讨论这个问题?

但是在JDK1.7中的话,是先进行扩容后进行插入的,就是当你发现你插入的桶是不是为空,如果不为空说明存在值就发生了hash冲突,那么就必须得扩容,但是如果不发生Hash冲突的话,说明当前桶是空的(后面并没有挂有链表),那就等到下一次发生Hash冲突的时候在进行扩容,但是当如果以后都没有发生hash冲突产生,那么就不会进行扩容了,减少了一次无用扩容,也减少了内存的使用。

CONCURRENTHASHMAP有什么优势以及1.7和1.8区别

Concurrenthashmap线程安全的,1.7是在jdk1.7中采用Segment + HashEntry的方式进行实现的,lock加在Segment上面。1.7size计算是先采用不加锁的方式,连续计算元素的个数,最多计算3次:1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;2、如果前后两次计算结果都不同,则给每个Segment进行加锁,再计算一次元素的个数;

1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount,通过累加baseCount和CounterCell数组中的数量,即可得到元素的总个数;

JDK1.7中,ConcurrentHashMap要进行两次定位,先对Segment进行定位,再对其内部的数组下标进行定位。定位之后会采用自旋锁+锁膨胀的机制进行加锁,也就是自旋获取锁,当自旋次数超过64时,会发生膨胀,直接陷入阻塞状态,等待唤醒。并且在整个put操作期间都持有锁。

而在JDK1.8中只需要一次定位,并且采用CAS+synchronized的机制。如果对应下标处没有结点,说明没有发生哈希冲突,此时直接通过CAS进行插入,若成功,直接返回。若失败,则使用synchronized进行加锁插入。

3.计算size的方法不一样

1.7:采用类似于乐观锁的机制,先是不加锁直接进行统计,最多执行三次,如果前后两次计算的结果一样,则直接返回。若超过了三次,则对每一个Segment进行加锁后再统计。

1.8:会维护一个baseCount属性用来记录结点数量,每次进行put操作之后都会CAS自增baseCount

JDK1.7中的实现:

ConCurrentHashMap 和 HashMap 的put()方法实现基本类似,所以主要讲一下为了实现并发性,ConCurrentHashMap 1.7 有了什么改变

需要定位 2 次 (segments[i],segment中的table[i])

由于引入segment的概念,所以需要

先通过key的 rehash值的高位 和 segments数组大小-1 相与得到在 segments中的位置

然后在通过 key的rehash值 和 table数组大小-1 相与得到在table中的位置

没获取到 segment锁的线程,没有权力进行put操作,不是像HashTable一样去挂起等待,而是会去做一下put操作前的准备:

table[i]的位置(你的值要put到哪个桶中)

通过首节点first遍历链表找有没有相同key

在进行1、2的期间还不断自旋获取锁,超过 64次 线程挂起!

JDK1.8中的实现:

先拿到根据 rehash值 定位,拿到table[i]的 首节点first,然后:

如果为 null ,通过 CAS 的方式把 value put进去

如果 非null ,并且 first.hash == -1 ,说明其他线程在扩容,参与一起扩容

如果 非null ,并且 first.hash != -1 ,Synchronized锁住 first节点,判断是链表还是红黑树,遍历插入。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值