1.在上篇HashMap博客中,已经提到jdk7使用的是数组+链表来实现,而jdk8使用数组+链表+红黑树实现,那为什么要引入红黑树呢,原因就是哪怕将hash的碰撞降到最低,也不能避免链表会越来越长。
[1].链表属于插入快,遍历慢的数据结构
[2].完全二叉树插入慢,遍历快
[3].红黑树插入和查询都较快
[4].jdk7中的put方法
public V put(K key, V value) {
// HashMap允许存放null键和null值。
// 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
if (key == null)
return putForNullKey(value);
// 根据key的keyCode重新计算hash值。
int hash = hash(key.hashCode());
// 搜索指定hash值在对应table中的索引。
int i = indexFor(hash, table.length);
// 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
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))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果i索引处的Entry为null,表明此处还没有Entry。
modCount++;
// 将key、value添加到i索引处。
addEntry(hash, key, value, i);
return null;
}
[5].jdk8中的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash
* hash for key
* @param key
* the key
* @param value
* the value to put
* @param onlyIfAbsent
* if true, don't change existing value
* @param evict
* if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
// 如果当前map中无数据,执行resize方法。并且返回n
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 如果要插入的键值对要存放的这个位置刚好没有元素,那么把他封装成Node对象,放在这个位置上就完事了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 否则的话,说明这上面有元素
else {
Node<K, V> e;
K k;
// 如果这个元素的key与要插入的一样,那么就替换一下,也完事。
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 1.如果当前节点是TreeNode类型的数据,执行putTreeVal方法
else if (p instanceof TreeNode)
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {
// 还是遍历这条链子上的数据,跟jdk7没什么区别
for (int binCount = 0;; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 2.完成了操作后多做了一件事情,判断,并且可能执行treeifyBin方法
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) // true || --
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 判断阈值,决定是否扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2.hash算法简化
[1].jdk7的hash算法
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
[2].jdk8中的hash算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
[3].为什么在jdk8中要简化hash算法:jdk8之前之所以hash方法写的比较复杂,主要是为了提高散列行,进而提高遍历速度,但是jdk8以后引入红黑树后大大提高了遍历速度,继续采用复杂的hash算法也就没太大意义,反而还要消耗性能,因为不管是put()还是get()都需要调用hash()
3.新节点插入顺序:jdk7在头部插入,jdk8在尾部插入
[1].这里需要提一下两个jdk8新增的属性TREEIFY_THRESHOLD是树化阀值,TREEIFY_THRESHOLD是链表化阀值,当链表上的数据大于等于树化阀值,链表转化为红黑树,反之小于等于链表化阀值时转为链表
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD= 6;
[2].因为并没有对每个链表(红黑树)进行记录元素个数,所以每次都是通过遍历得来的元素个数,所以在遍历完后就顺带插入到尾部
4.扩容机制
[1].jdk7中的扩容
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
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;
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;
}
}
}
[2].核心就是transfer()方法,2个循环
①.对索引数组中的元素遍历
②.对链表上的每一个节点遍历:用 next 取得要转移那个元素的下一个,将 e 转移到新 Hash 表的头部,使用头插法插入节点(所以元素的位置在链表中的物理体现是跟之前反着的)。
③.循环2,直到链表节点全部转移
④.循环1,直到所有索引数组全部转移
[3].问题:在多线程下当2个线程操作统一条链表就有可能出现链表闭合
①.当两个线程调用put()方法,且都满足扩容的条件(new了两个新数组),其中一条链表是3→5

②.线程一进到transfer(),把 Entry<K,V> next = e.next;执行完就挂起了,当前e记录的是3,next是5
③.线程二进到这个方法将扩容完成,此时由于jdk7的扩容后链表会反转,所以是5→3

④.线程一被唤醒,将e(3)插到线程一的某个链表的表头,把next(5)值赋给e

⑤.由于5的下个节点是3,故把3赋给next,将e(5)插到线程一对应链表的表头,现在的指向是3→5,把next(3)赋给e

⑥.e的next为null(不重要),把e(3)再指放到链表的表头,把3指向5,此时,闭环就形成了,3的next是5,5的next也是3

[4].由于jdk8是顺序拷贝,所以就不会产生jdk7的死锁
[5].jdk7扩容后hash值会发生变化,因为hashSeed值随着容量增长,,而jdk8根据该扩容长度调整存元素放位置
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;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
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;
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)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
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;
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) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
1089

被折叠的 条评论
为什么被折叠?



