标签:作用 htable 位置 通过 详解 总结 hash算法 ++ 内存
三者都是存储键值对的Key-Value
key会被映射到数组索引, Entry对象则是数组中对应的值。
Key通过Hash算法得到哈希码(HashCode), 通过哈希码与数组中的索引对应。
因此所有的键值对Hash表都是无序储存的。
键值对的查找过程: (hashCode()计算出索引,通过键的equals方法找到对应的键值对, 返回值对象)先对键做一个hashCode()的计算来得到它在bucket数组(这里的计算方法略有区别)中的位置来存储Entry对象。当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象。
负载因子为什么是0.75而不是1或者0.5?
因为如果是0.5的话, 散列表会比较稀疏, 更大程度上避免了哈希碰撞的可能性, 索引效率高, 此时会对空间造成浪费
如果是1的话, 散列表会变得密集, 空间利用率提高了, 但是哈希碰撞的可能性变大了, 链表和红黑树会变得复杂, 索引效率会变低
经检测, 0.7-0.75的索引效率是最高的
采用数组和链表, 1.8依然是数组和链表
线程安全的:使用synchronized来保证线程安全, 但是读写的时候会锁住整张表
扩容机制: HashTable的初始大小是11,扩容通过rehash()来实现, 当HashTable的数组元素个数达到临界值的时候, 就会调用rehash来进行扩容, 扩容后的大小是2*oldSize + 1, 临界值就是数组大小乘以负载子(loacFactor) , 通常负载因子默认为0.75。因此数组元素个数达到数组大小的0.75倍时, 就会进行扩容。
计算index方法:(取模方法) index = (hash & 0x7FFFFFFF) % tab.length,采用取模方法,减少哈希碰撞, 使数组分布更均匀, 但是效率比HashMap的位运算低。
链表依然采用头插法,而没有像HashMap在jdk1.8中为了避免并发情况下扩容死锁改为尾插法,因为头插法的效率比尾插法更高,同时HashTable是线程安全的, 并发下不会出现扩容死锁的情况。
线程不安全的
扩容机制:初始容量默认16, 元素个数达到临界值后扩容, 新的容量为 2*oldSize, 大小一定为2的n次幂 , 临界值依然是数组容量 * loadFactor(负载因子)
扩容resize():
1.初始化数组table
2.当数组table的size达到阙值时即++size > load factor * capacity 时,也是在putVal函数中
具体实现
1.通过判断旧数组的容量是否大于0来判断数组是否初始化过
是,进行扩容,扩容成两倍(小于最大值的情况下),之后在进行将元素重新进行与运算复制到新的散列表中
put的过程
为什么扩容必须是2的幂
计算index方法:index = hash & (tab.length – 1)
线程安全的, jdk1.7利用分段锁来提高并发程度, jdk1.8采用CAS和synchronized 来保证并发线程安全
ConcurrentHashMap的键和值不能为空
扩容机制:
jdk1.7的扩容机制:
jdk1.8的扩容机制:
sizeCtl 和 ForwordNode 在扩容中起关键作用
- sizeCtl :默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。
- -1 代表table正在初始化
- -N 表示有N-1个线程正在进行扩容操作
- 其余情况:
- 如果table未初始化,表示table需要初始化的大小。
- 如果table初始化完成,表示table的容量,默认是table大小的0.75倍
当数组元素个数超过临界值的时候, 就会进行扩容操作, 同时将sizeCtl的值修改。扩大后的长度和HashMap是一样的, 依然是原数组容量的2倍。扩容操作开始之后, 会将旧数组上的值迁移到新数组中,迁移完成的节点和空节点会被设置成ForwordNode节点, 其他线程进行读写操作时, 若节点是ForwordNode节点 , 也会加入到扩容操作。如果节点是正常节点, 读操作(get方法)依然和之前一样进行读操作, 写操作(put和remove)如果当前索引如果没有出现哈希碰撞, 采用CAS+自旋的方式进行写操作, 如果有哈希碰撞, 则进行扩容操作。
HashMap、HashTable、ConcurrentHashMap详解
标签:作用 htable 位置 通过 详解 总结 hash算法 ++ 内存
原文地址:https://www.cnblogs.com/jiangblog/p/13214195.html