HashMap采用的数据结构
加载因子
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
加载因子主要用来计算阈值(capacity * loadFactor),如果设置为1,那么阈值即为数组的长度,key-value数只有大于数组长度才会扩容,这样的话减少了空间开销,但是同时,链的长度会过长,查找的效率会低。如果设置为0.5,那么会出现多次扩容,这样链表长度会较低,查询效率会较高,但是数组中会出现空余的空间,那么空间利用率较低。
结论:设置为0.75保证了空间利用率和查询复杂度的平衡。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hash值为hashCode的高16位和低16位进行异或运算,降低hash冲突率。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
获取当前的阈值,如果当前容量大于了阈值,则开始扩容,阈值为大于数组长度的最小2的n次方数。如设置的数组长度为10,那么得到的阀值为16。即如果当前map中key-value数大于该阈值,则开始扩容。
添加key value值,源码注释
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)
//hash后生成的索引值在数组中一个值也没有找到,在该索引值的位置上插入key和value节点
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//hash生成的索引值在数组中找到了值,赋值给e
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);
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))))
//在列表中找到相同的hash和key值。设置为e的值
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//e为已存在的节点,更新该节点的值。
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
//当前节点数大于阀值
resize();
afterNodeInsertion(evict);
return null;
}
在定位数组索引数时使用了按位与运算,不用取模运算是为了提高效率,如果采用了取余,那么就意味着底层将会转化为十进制。存在如下公式
X % 2^n = X & (2^n – 1)
其中n为任意整数,2^n即为数组的长度,那就意味着我们在构造函数中设置capactiy时一定要为2的指数次幂,那么(X & (2^n – 1))就不是取余运算。
参考:
https://blog.csdn.net/hollis_chuang/article/details/103452727
如果链表的长度大于一定的阈值并且数组的长度大于64,则转为红黑树,如果数组长度不大于64那么只是会执行扩容,table初始化和扩容代码在同一个方法中。
树化的方法中,如果tab容器小于64,就会进行扩容,而不会执行链表的树化。
红黑树性质
1 一个节点要么为红色,要么为黑色
2 根节点为黑色
3 如果一个节点为红色,则两个子节点一定为黑色。
4 从任意一个节点出发到达任意叶子节点,黑色节点数相同。
扩容
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
//调用构造方法public HashMap(int initialCapacity, float loadFactor),
//初始化时,构造方法中通过入参capacity得到了阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//调用构造方法,初始化没有提供capacity,数组长度和阈值均用默认的
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;
//将旧tab中存储的数据重新进行hash计算,放到新的tab中
@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;
}
新的tab长度为之前的两倍,阈值也为之前的两倍
代码中处理链表分为需要移动到其他位数的节点和不需要移动的节点
需要移动的节点推理如下
根据hash&2^N==0可以推理出hash的第n+1位为0
hash&(2^(N+1)-1)为获取hash的N+1位
hash&(2^N-1)为获取hash的N位
因为hash的第n+1位为0
所以可以推理出hash&(2^(N+1)-1)= hash&(2^N-1)
所以hash值满足hash&2^N==0的时不需要移动到其他索引位置的。
根据hash&2^N!=0可以推理出hash的第n+1位为1
hash&(2^N-1)为获取hash的N位
hash&(2^(N+1)-1)为获取hash的N+1位
因为hash的第n+1位为1
所以hash&(2^N-1)+2^N=hash&(2^(N+1)-1)
所以hash&2^N!=0的节点需要放到之前索引的位置加上之前容量大小,即为新table的位置。
顺
便
总
结
一
下
H
a
s
h
M
a
p
和
H
a
s
h
T
a
b
l
e
之
间
的
区
别
,
\color{#FF0000}{ 顺便总结一下HashMap和HashTable之间的区别,}
顺便总结一下HashMap和HashTable之间的区别,
HashMap 数组i的定位是先通过hashCode的前16位和后16位执行异或运算得到hash值,
然后执行 hash&(数组长度-1)得到数组位置i
Hashtable 数组i的定位采用 (hashCode&Integer.MAX)%table.length,hashCode有可能为负数。
Hashtable在链表中采用的是头插法,HashMap采用的是尾插法。
扩容的话都是数组长度扩大两倍。
ConcurrentHashMap
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//计算Hash值,高16位和第低16位做异或运算,
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初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
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)
//当前有线程在扩容,当前线程去帮助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
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;
}
协助扩容逻辑
/**
* Helps transfer if a resize is in progress.
*/
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
//当前正在扩容,并且新的tab已经初始化了。
//根据之前tab长度拿到本次扩容的标识,
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
//(sc >>> RESIZE_STAMP_SHIFT) != rs说明正在进行的扩容与当前线程执行的扩容是扩容同一个tab
//sc == rs + 1 || sc == rs + MAX_RESIZERS 这个判断有bug。详见https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
//开始扩容。sizeCtl低16位存储正在扩容的线程数-1
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
/**
* Tries to presize table to accommodate the given number of elements.
*
* @param size number of elements (doesn't need to be perfectly accurate)
*/
private final void tryPresize(int size) {
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
//tab初始化
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
int rs = resizeStamp(n);
if (sc < 0) {
//sc小于0说明已经开始扩容
Node<K,V>[] nt;
//和扩容中的判断一样
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
//说明当前线程为第一个参与扩容的线程,sizeCtl需要加2,
//结束的时候通过sizeCtl-2是否等于resizeStamp(n) << RESIZE_STAMP_SHIFT来
//判断当前线程是否为最后一个线程,做好收尾工作。
transfer(tab, null);
}
}
}
扩容
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
//计算每个线程负责扩容的数组长度
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//初始化扩容后的数组长度
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//获取从右向左开始获取需要扩容的数组索引,i为接下来需要扩容的节点。bound 为当前线程结束扩容的索引。
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
//i比当前线程结束迁移的位置还大,继续继续迁移i处的数据,这里是跳出当前循环,
//finishing==true,当前线程的任务完成了,结束
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
//transferIndex记录最后一个线程帮忙迁移的索引,小于等于0,说明迁移已经完成。结束。
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
//计算当前线程下一次迁移的范围区间。
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//判断当前是否已经结束扩容
if (i < 0 || i >= n || i + n >= nextn) {
//当前线程已经完成了迁移任务,并且所有节点均迁移完成,结束吧
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
//迁移完成--sizeCtl重新回到记录扩容阀值。
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
//重新检查一下,看那些还没迁移完。
i = n; // recheck before commit
}
}
//为空节点赋值为forwardNode,标识正在扩容。
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
//当前节点已经迁移了
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
//找出从最后一段开始、均为高位或低位的节点。
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
//从头开始一直遍历到最后一段连续的节点,利用头插发连接起来
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
//之前的节点设置迁移节点,标识迁移完成。
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
树化
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
//大于小于64说明数据在数组中较为集中,需要扩容,而不是树化。
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
sizeCtl:记录数组的大小默认值为16,记录扩容的标准是12,还可以为负数,-1标识正在tab初始化,
如果正在扩容中,sizeCtl前16标识本次扩容,后16位标识正在参与扩容的线程数量。
table加valotile关键字只能保证拿到的是最新的数组地址,不能保证数组中是最新的数据。
ConcurrentHashMap节点链表不一定大于8就一定转为树化。只有数组的长度大于64,才进行树化,如果数组长度小于64,就进行扩容。
为啥数组长度要为2的N次幂,这样长度减一,即为n个1,与key的hash进行与运算时,能较大分配到数组的各个位置。
如果不为1的话,比如为1110,这样前三位相同,但是后1位均为0,这样就会落到同一个索引中。
对数组下面的节点保证线程安全,用synchronized保证。
ConcurrentHashMap key-value计数
采用baseCount+数组,对baseCount执行CAS失败,才会对数组随机索引中位置执行加1。执行size时将会变量数组中每个CellCount存储的数据加起来然后加上baseCount存储的数据。
LinkedHashMap略讲
继承自HashMap,有自己的一套节点结构,在HashMap节点结构的基础上添加的前置指针和后置指针。put、get操作都是用的HashMap中的方法,只不过LinkedHashMap重写了newNode方法,每new一个节点,会将节点添加到双向链表中,如果再次访问,会将节点移动到链表尾部,核心方法注释如下:
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
//处理好p节点前后节点的后置、前置指针指向
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
//被移除的节点为头节点--让头节点指向e后面的节点
head = a;
else
//前置节点指向p后面的节点
b.after = a;
if (a != null)
//后置节点不为空,让后置节点的前指针指向p的前置节点
a.before = b;
else
//后置节点为空,last指针指向p的前节点
last = b;
//p指针添加到新的位置中
if (last == null)
//last指向空,说明p为唯一一个节点
head = p;
else {
//p节点添加到last节点后面
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
执行淘汰的方法注释如下:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
找到该节点,然后断开节点即可。
LinkedHashMap存储排序好了的数据
Map<String,Integer> map=new HashMap<>();
map.put("a",1);
map.put("c",3);
map.put("b",2);
map.put("d", 0);
HashMap<String, Integer> sortMap = new LinkedHashMap<>();
map.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getValue))
.collect(Collectors.toList()).forEach(ele -> sortMap.put(ele.getKey(), ele.getValue()));
for (Map.Entry<String, Integer> entry : sortMap.entrySet()) {
System.out.println(entry.getKey()+"-"+entry.getValue());
}
20220514补充
LinkedHashMap变量本质变量的是链表
LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("1", 1);
linkedHashMap.put("3", 3);
linkedHashMap.put("2", 2);
//添加的顺序
//LinkedEntryIterator
Iterator<Map.Entry<String, Integer>> iterator = linkedHashMap.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, Integer> entry = iterator.next();
System.out.println(entry.getKey() + "=" + entry.getValue());
}
2022-0318补充
ConcurrentHashMap是弱一致性,虽然数组是用volatile修饰的,只能保证线程获取到的地址是最新的,不能保证数组中元素的地址是最新的,如果新加入一个Node节点,其他线程不见得能看到,HashTable就能保证,HashTable是强一致性。
参考:
https://blog.csdn.net/blingfeng/article/details/79855445
https://www.jianshu.com/p/3e6f308bc8a2
https://www.jianshu.com/p/39b747c99d32
https://www.cnblogs.com/Profound/p/10930523.html
https://www.jianshu.com/p/749d1b8db066
https://www.jianshu.com/p/88881fdfcf4c
https://www.jianshu.com/p/2829fe36a8dd
https://segmentfault.com/a/1190000012964859