hashMap简介
HashMap是实现了map接口的非线程安全结构,基于hash原理,存储键值对对象,键(key)与值(value)都可以为null。JDK7中采用数组+链表的方式来实现,即使用拉链法来解决冲突,经过hashCode方法得到同一hash值的对象都存储在一个链表里,但当位于同一个backet数组中的元素较多时,通过key依次查找的效率较低(O(n));JDK8中采用数据+链表+红黑树来实现,当一个backet数组中的元素达到阀值时,会将链表转变为红黑树进行处理,这样大大减少了查找时间(O(logn))。
hashMap工作原理
hashMap首先含有一个backet数组,初始大小为16,通过对元素hash值的计算来确定要放入/读取backet数组中的位置。放入(put)一个元素时首先根据hashCode方法计算其hashCode值,根据hashCode值来确定在backet数组中的位置,若该位置处没有元素,直接创建节点插入,若该位置中存在元素,由于采用链地址法来解决hash冲突,所以将元素插入链头(即新加入的在链头,后加入的在链尾)。hashMap会根据backet数组中元素的占用情况来动态调整数组大小(负载因子=元素个数/backet数组大小=0.75,当超过0.75时,会将新数组调整为原数组的两倍)。读取(get)一个元素时,会根据hashCode方法来计算hashCode值进而确定backet数组位置,再根据equals方法来确定键值对。在JDK8中,当一个链表的元素长度大于阀值(默认为8),会动态调整为红黑树结构,读取速度加快。
hash原理
使用hash方法来确定backet数组中的位置。具体步骤:1.取key的hashCode值,并与(数组.length-1)进行取模运算(JDK7),并且数组长度总为2的n次方2.取key的hashCode值,并与高16位进行异或运算(JDK8),最后再与(数组.length-1)进行取模运算.
JDK8相较于JDK7改进的优势:在数组table较小的时候,也能保证高低位bit都参与到hash的计算中,同时不会有较大开销。
//方法一:
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//方法二:
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return h & (length-1); //第三步 取模运算
}
put方法
1.根据hash方法计算其hashCode的值进而得到其在backet数组中的位置
2.判断这个backet是否存在,若不存在创建并进行长度初始化
3.若这个位置没有发生碰撞则直接插入
4.若发生碰撞则先判断这个位置是否已经转换为红黑树结构,若是则项红黑树结构插入元素,否则插入链表中
5.若插入数据导致链表长度超过默认阀值(8),将链表转变成红黑树
6.如果要插入节点已经存在,则直接替换(保证key唯一性)
7.查看backet数组是否已经超过负载因子,若超过需要重新resize
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
*生成hash的方法
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断table是否为空,
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//创建一个新的table数组,并且获取该数组的长度
//根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//如果对应的节点存在
Node<K,V> e; K k;
//判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 该链为链表
else {
//遍历table[i],判断链表长度是否大于TREEIFY_THRESHOLD(默认值为8),大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 写入
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
get方法
1.判断backet数组是否为空,为空直接返回
2.判断数组中的第一个节点是否是要找的元素,命中直接返回
3.若第一个元素不是,则说明存在冲突,查看该位置链为红黑树或是链表
4.根据判断的结构的不同,去调用equals去得到要找到的节点
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;//Entry对象数组
Node<K,V> first,e; //在tab数组中经过散列的第一个位置
int n;
K k;
/*找到插入的第一个Node,方法是hash值和n-1相与,tab[(n - 1) & hash]*/
//也就是说在一条链上的hash值相同的
if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {
/*检查第一个Node是不是要找的Node*/
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))//判断条件是hash值要相同,key值要相同
return first;
/*检查first后面的node*/
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
/*遍历后面的链表,找到key值和hash值都相同的Node*/
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
resize(扩充)方法
hashMap的初始值长度为16,每次扩充的时候都是扩充为原来的两倍,因此在将就数组中的值rehash后放如新数组中,数组可能会在:原位置或者是:原位置+oldCap(原数组容量)。这样的话链表不会倒置或者发生竞态条件(JDK1.7中会出现)。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
/*如果旧表的长度不是空*/
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
/*把新表的长度设置为旧表长度的两倍,newCap=2*oldCap*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
/*把新表的门限设置为旧表门限的两倍,newThr=oldThr*2*/
newThr = oldThr << 1; // double threshold
}
/*如果旧表的长度的是0,就是说第一次初始化表*/
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;//新表长度乘以加载因子
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
/*下面开始构造新表,初始化表中的数据*/
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;//把新表赋值给table
if (oldTab != null) {//原表不是空要把原表中数据移动到新表中
/*遍历原来的旧表*/
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)//说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
/*如果e后边有链表,到这里表示e后面带着个单链表,需要遍历单链表,将每个结点重*/
else { // preserve order保证顺序
新计算在新表的位置,并进行搬运
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;//记录下一个结点
//新表是旧表的两倍容量,实例上就把单链表拆分为两队,
//e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {//lo队不为null,放在新表原位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
hashMap中的Fail-fast机制
hashMap是线程不安全的,所以当多线程对map进行结构改变(增加或删除)时,会抛出ConcurrentModificationException异常。
1.hashIterator迭代方法时exceeptModCount=modCount
2.在迭代过程中判断出exceeptModCount!=modCount,抛出异常
HashTable与ConcurrentHashMap比较
Hashtable与ConcurrentHashMap都是HashMap的线程安全版本。Hashtable对HashMap的每个方法进行加锁,这样当Hashtable数量增大到一定程度上的时候,性能会急剧下降。
ConcurrentHashMap采用分段锁的方式,将map分为16个Segment,每个Segment的容量也为16,put与get方法会经过3次hash来确定其最终位置
1.先对key进行第一次hash,得到hash值h1,即h1=hash(key)
2.对h1的高位进行hash得到h2,根据h2来确定其在哪一个Segment,此时为第二次hash,即h2=hash(h1高几位)
3.对h1进行第三次hash得到h3,根据h3来确定该元素放到哪个HashEntry(只有value可变,并且为volatile)
ConcurrentMap中读元素是不用加锁的,而写数据是需要加锁的,删除元素的时候需要将数据进行复制
ConcurrentMap不能代替HashTable。
因为ConcurrentHashMap是弱一致性的迭代器,而HashTable是强一致性迭代器。即在多线程环境下,当ConcurrentMap加入一个元素后,可能并不能通过get立马获得(同时进行put与get操作),这是因为get方法没有加锁的缘故。
参考文章:
http://blog.youkuaiyun.com/tuke_tuke/article/details/51588156
http://blog.youkuaiyun.com/richard_jason/article/details/53887222
http://blog.youkuaiyun.com/xuehuagongzi000/article/details/71449179
http://zhangshixi.iteye.com/blog/672697