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节点,判断是链表还是红黑树,遍历插入。