要将其存入到HashMap中,就需要确定key,value组成的Node对象在数组索引下标中的位置。
Map中数组初始大小为16,即索引位置为0-15
将key转换成数据中的索引位置:
public native int hashCode();
但是直接对一个数进行取模运算并不高效,可以将数值转换成二进制,然后进行&运算,这样更高效
示例:
"java".hashCode(); //结果返回3254818
将3254818装换成二进制,然后与15(length-1)的二进制01111进行 & 运算,可以很高效的计算得出结果为8。
任何一个二进制数值与01111进行 & 运算,最大值为1111,最小值为0000。换算成十进制,最大值为15,最小值为0,结果和直接与16进行取模运算一样。
11 0010 0101 0100 1000 0001 1000
00 0000 0000 0000 0000 0000 1111 &
00 0000 0000 0000 0000 0000 1000 结果为8
根据上述规则,产生一个新的问题是,如果两个key不相同,但是求得hashCode的值换算成二进制时最后4位是相同的,那么最终得到的索引位置是相同的。这样位置就会冲突。
期望:不同的key计算得到的索引尽可能是不一样的。
根据上述进行 & 运算的规则,可以影响计算出的index结果的只有key.hasCode计算得到的值的最后几位,那么就需要让参与 & 运算的规则最后几位的值尽可能的不一样。
结合key.hasCode返回的结果是int型,int转换成二进制一定是一个0-32位之间的一个数,因此将求得的hashCode值一分为二,拆成高16位和低16位,然后将高16位和低16位进行 ^(异或) 运算得到一个新的结果,然后再参与 & 运算,这样计算得到的索引值就会尽可能的减低重复率。即,key.hashCode() ^ key.hashCode() >>> 16
0000 0011 0010 0101
0100 1000 0001 1000 ^
0100 1011 0011 1101
将求得的hashCode进行高低16位的异或操作后,在计算得到的索引势必还是会重复,这种情况下,只能按链表和红黑树进行存储。
按照 & 运算的规则,想要得出的结果只受参与运算的ope1决定,那么ope2必须全是1才行。
ope2=length-1,那么length必须是最高位是1,其余位全是0才行,因此length必须是2的n次幂才行。
因此数组的长度一定是2的n次幂
在put的时候,共需要以下几个步骤
注意:
链表的长度超过8,就转为红黑树;红黑树里的节点小于6,就转为链表。
在扩容时要保证2的倍数扩容,比如16 —> 32,符合2的幂次的规律。
public V put(K key, V value) {
//先对key进行hashCode运算,得到index值
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//如果数据没有初始化,则进行数据初始化
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//根据计算得到的索引,判断当前位置有没有值,如果没有,则放入当前位置
tab[i] = newNode(hash, key, value, null);
else {
/*
对应索引的位置已经有元素了,初步想法是分为三种情况:
1.如果key相同,则直接将索引位置的元素替换;
2.key不相同,想要以链表的方式存储
2.key不相等,想要以红黑树的方式存储
*/
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//情况1,key相同,hash值相同,进行替换
e = p;
else if (p instanceof TreeNode)
//红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); //单项链表,尾部插入发,Node节点的形成
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;
if (++size > threshold)
//1.当数组的长度超过一定限制后,就会进行扩容。
//2.扩容完成后,还要进行重新散列,即将老数组中的数据移动到新的数组
//threahold=数据长度*0.75
resize();
afterNodeInsertion(evict);
return null;
}
为什么数组下面存储元素,要先以链表的方式进行存储,当链表长度超过8之后,就会将链表转换成红黑树?为什么不直接使用红黑树?
导致问题的根因就是,对构建链表和红黑树的空间、时间复杂度的取舍。
链表和红黑树出现的本质是:数组、链表、红黑树的性能不同。
HashMap想要线程安全最简单的方式就是给put方式加上synchronize关键字。而HashTable就是这样做的。
给整个puta方法加上锁的话,当一个线程去put一个值的时候,其余线程再put值的时候就只能等待。但是可能线程1和线程2操作的并不是同一个索引位置的数据。这样直接全部锁住不太合理。那么具体该如何操作呢?
ConcurrentHashMap已经帮我们实现了这种操作。
ConcurrentHashMap在put的时候,首先保证了数组初始化的线程安全。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//第一个线程在执行数组初始化之前,调动sun.misc.Unsafe类的compareAndSwapInt方法通过乐观锁(cas)的机制保证初始化数组的线程安全
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
当index==null时,同样采用cas的方式保证了线程安全;
当index!=null是,由于链表、红黑树结构的变化,继续采用cas的方式保证线程安全的话,需要比较的次数就会增大,造成性能的浪费。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//在进行put值的时候,当index==null时,也是通过cas的机制保证线程安全
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
//MOVED 当有线程在进行数组扩容时,会被置为-1,然后其余线程进行put时,检测到该值就会帮助其进行扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//当index!=null时,通过加锁的方式来保证线程安全。锁对象是当前数组index位置的对象,这样就保证了index位置下面链表或红黑树操作的安全性。对于其他index位置不受影响
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
扩容是一定会有线程安全的问题的,因为扩容时进行了两步操作
当线程1进行扩容时,其余线程是不能进行put的,除线程不安全外,即使put了以后,在线程1扩容之后,也会重新改变index。但是如果其余线程等待的话,就需要干等。于是ConcurrentHashMap进行了如下设计: