Java78中HashMap和ConcurrentHashMap源码阅读
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java78中HashMap和ConcurrentHashMap源码阅读相关的知识,希望对你有一定的参考价值。
首先来看下HashMap的类继承结构:
public class HashMap extends AbstractMap<K,V> impement Map<K,V>,Coloneable,Serializable{
}
可以看出HashMap实现了Map接口。其里面的方法都是非线程安全的,且不支持并发操作。
对于HashMap主要看的是get/put方法实现,其在jdk1.7,及1.8在解决哈希冲突的上有所不同。
一、Java7 HashMap
从上面的结构图中,可以大致看出,HashMap由数组:transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;没个元素对应为一个单向链表,链表数据结构如下:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
在HashMap中定义的成员变量:
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
loadFactor:负载因子,默认为 0.75。
threshold:扩容的阈值,等于 capacity * loadFactor,当容量超过这个值时,数组将扩容。
transient int modCount; //HashMap修改次数,这个值用于和expectedModCount期望修改次数比较。
1、put方法解析:
public V put(K key, V value) {
//1.当插入第一个元素时,需要创建并初始化指定大小的数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//2.如果 key 为 null,循环遍历table[0]上的链表,最终会将这个 entry 放到 table[0] 中
if (key == null)
return putForNullKey(value);
//3.计算key的哈希值
int hash = hash(key);
//4、通过h & (length-1)即h%length求模找到键值对放在哪个位置。
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//循环对应位置的单向链表,逐个查找每个链表节点上是否有整个键了。
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//hash值为整数,比较性能比equals高;另外短路运算,哈希值系统了就没必要在比较equals。
V oldValue = e.value;//先将当前节点的键对应的值取出来。
e.value = value; //替换为新值。
e.recordAccess(this);
return oldValue;
}
}
modCount++; //容器修改次数加1
addEntry(hash, key, value, i); //在指定的位置上添加数据,若空间不够则动态扩充,当前容量乘以2,新建一个数组,长度为capacity*2;并将原来的数组拷贝过来,更新对应变量。
return null;
}
数组初始化:
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize); //指定数组容量,默认为16
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity]; //改变数组的引用,指向新创建的数组
initHashSeedAsNeeded(capacity);
}
计算键值对的位置:
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1); //等同于求模:h%length
}
添加节点到链表中
void addEntry(int hash, K key, V value, int bucketIndex) {
//假如map的元素个数大于等于阈值,并且bucketIndex的位置已经元素,则需要动态扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
//重新计算应该存储下标
bucketIndex = indexFor(hash, table.length);
}
//创建元素及链表节点
createEntry(hash, key, value, bucketIndex);
}
//新建一个Entry对象,插入单向链表表头,并增加size(不论是否扩容这一步都要进行)
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
数组扩容:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//新建一个容量扩充2倍的数组
Entry[] newTable = new Entry[newCapacity];
//调用transfer方法将旧数组中的键值对拷贝过来
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//旧数组原来的堆空间设置为引用切断,指向新数组
table = newTable;
//重新计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
键值对移植:
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
//是否重新计算key的哈希
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//重新计算元素位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
以上就是保存键值对的主要代码,基本步骤:
1)、计算key的哈希值;
2)、根据哈希值计算数组元素的保存位置(h&(length-1)或h%length);
3)、根据需要扩充数组大小;
4)、将键值对插入到对应的链表头部或更新已有值;
2、get方法解析
public V get(Object key) {
//如果key为空则直接,在存放元素时是直接存放到table[0],所以直接调用getForNullKey方法遍历对应链表即可。
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
遍历table[0]位置的链表,返回对应key==null的值,若果返回null,则有两种情况,要么没有key==null的键值对,要么对应位置上的值为null。
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
key值不为空,则调用返回对应的值:
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
总结基本流程:
1、计算键的哈希值;
2、根据哈希值找到数组中对于的链表;
3、遍历链表,查找对应key的值;
4、在比较查找的过程中,先快速比较哈希值,hash相同则再继续通过equals比较;
二、java7 ConcurrentHashMap
在java7 下ConcurrentHashMap结构如下:
ConcurrentHashMap是并发版的HashMap,支持复杂的并发操作,通过降低锁的粒度和cas等实现了高并发,支持原子条件的更新操作,不会抛出ConcurrentModificationException,实现了弱一致性。
ConCurrentHashMap是一个Segment数组,每个segment元素对应一个哈希表(结构类似于HashMap)
未完待续....(ConcurrentHashMap没太读明白)
以上是关于Java78中HashMap和ConcurrentHashMap源码阅读的主要内容,如果未能解决你的问题,请参考以下文章
HashMap 和 ConcurrentHashMap,Java1.8版本
深入理解java:2.3.3. 并发编程concurrent包 之容器ConcurrentHashMap