HashMap
数据结构
JDK 1.6 数组 + 链表
JDK 1.8 开始 数组 + 链表 + 红黑树
引入链表结构解决hash冲突,JDK1.8开始采用尾插法(1.8以前头插法),链表时间复杂度O(n)
hash 并不是采用取模(%)运算,而是采用位&运算,从效率上来讲,位运算效率远高于%取模运算,位运算是最接近机器能识别的语言
虽然引入了红黑树,解决了链表成环问题,但效率并未得到大的提升,在数据量较小的时候效率甚至会更低,在数据量较大的时候效率大概有 5-10% 的提升
HashMap容量
定义: 数组的大小
new HashMap(): 如果不写构造参数,默认容量16,DEFAULT_INITIAL_CAPACITY = 1 << 4
注意: HashMap容量必须满足2n。比如 new HashMap(20),因为24 < 20 < 25,故将其初始容量设置为32。
HashMap容量必须满足2n>的原因:
① JDK1.8 高低位运算需要使用
②按位与高效替代取余也必须满足2n-1
roundUpToPowerOf2(size);
HashMap是线程不安全的。高并发场景下 get 操作也可能带来数据丢失。JDK1.8以下可能多线程情况下扩容可能发生链表成环,引发死锁问题。
HashMap保证有序性的扩展
源码分析
重要成员变量
DEFAULT_INITIAL_CAPACITY = 1 << 4; Hash表默认初始容量 16
MAXIMUM_CAPACITY = 1 << 30; 最大Hash表容量 230
DEFAULT_LOAD_FACTOR = 0.75f;默认负载因子
TREEIFY_THRESHOLD = 8;链表转红黑树阈值
UNTREEIFY_THRESHOLD = 6;红黑树转链表阈值
MIN_TREEIFY_CAPACITY = 64;链表转红黑树时 hash 表最小容量阈值,达不到优先扩容
JDK 1.8 链表转红黑树的两个条件:
- 链表长度 > 8
- 链表转红黑树时 Hash 表最小容量阈值,否则优先扩容
默认加载因子:DEFAULT_LOAD_FACTOR = 0.75f
数组使用率大于此百分比数值时会进行扩容,该值是综合实践与空间上的均衡后考虑的。全放满再扩容,容易发生 hash 碰撞,时间复杂度O(n),效率不高。如果小于此值,可能浪费空间。但 0.75 亦不是最优解,最优的考虑是基于概率问题的牛顿二项式,基于时间与空间后折中计算后大约为 0.693。
为什么链表长度大于 8 才开始扩容
源码中有注释,在默认加载因子0.75的情况下,根据概率分布泊松分布,当链表长度达到 8 出现的几率是千万分之6,之后的概率不到千万分之一(数据量极大时才会转换)
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
JDK 1.7 扩容链表成环导致死锁问题分析
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;
}
}
}
static int indexFor(int h, int length) {
return h & (table.lenth-1);
}
注意: 任意数与(&) 2n 只能有两种结果(如果table.length = 16 只可能得到0或16,还可能越界,故采用 h & (table.lenth-1),即能防止越界,还可以得到 table.length 个结果。
单线程情况下扩容:
假设 hash 表初始长度为 2,某一数组位置上有 key:{3,5,7},按照 Hash 算法取余运算(key%table.length)得到其都碰撞在table[1]上。
假设 hash 表长度扩容到 4,重新计算 hash 得到 table[3] : {3,7},table[1]:{5},当 e.next == null 时跳出循环,扩容结束,扩容过程如下:
多线程情况下扩容:
假设有两个线程同时进行 put ,并且同时进入 transfer() 方法,高并发环境下,极易导致链表成环的问题。
JDK8 的改进
- JDK8 对 HashMap 进行了优化,完全抛弃了原来到扩容方法,采用高低位拆分转移方式,避免了链表环的产生。
- 因为使用了高低位指针,完全绕开了rehash,必须满足高低位的移动,故数组扩容量必须是2n
- 引入红黑树
TREEIFY_THRESHOLD = 8;链表转红黑树阈值(长度大于等于 9 开始扩容)
UNTREEIFY_THRESHOLD = 6;红黑树转链表阈值
MIN_TREEIFY_CAPACITY = 64;链表转红黑树时hash表最小容量阈值,达不到优先扩容
resize方法部分源码分析:
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;
}
ConcurrentHashMap
数据结构
JDK1.7 分段锁(Segment 数组) + 链表 + CAS + 红黑树
JDK1.8 分段锁(Node<K,V> 锁住链表(桶Bucket))+ 链表 + CAS + 红黑树
写(put)同步(有Hash碰撞才同步),读 (get) 无锁,加锁时是给第一个Hash表某一 Segment 加锁,实现分段锁的效果,不同的 Segment 的 put 操作也是可以并发执行的
JDK 1.7 与 JDK1.8 在实现 ConCurrentHashMap 与 HashTable 区别
HashTable: 全局锁,直接锁住整个 Hash 表
JDK 1.7 ConCurrentHashMap: 分段锁(Segment 数组) + 链表 + CAS ,大的 Hash 表里面套小的 Hash 表,小的 Hash 表里面存储数据当加锁的时候锁住某个小的 Hash 表
Segmen<K,V> 继承了 ReentrantLock,所有天生代加锁、解锁操作,Segment 是一种可重入锁,HashEntry存储键值对
JDK 1.7 HashMap结构(未展示红黑树部分)
Segment<K,V> s0 = new Segment<K,V>(loadFactor,(int cap * loadFactor)),
(HashEntry<K,V>][]) new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V> []) new Segment[ssize];
JDK 1.8 ConCurrentHashMap: JDK1.8 分段锁(Node<K,V> 锁住链表(桶Bucket))+ 链表 + 红黑树 + CAS + synchronized
JDK1.8 抛弃了 Segment 分段锁,采用1.8 采用 Node + CAS + synchronized 来保证并发安全进行实现,锁粒度更细,只锁单个链表(Node[i])
JDK 1.7 HashMap结构
for (Node<K,V>[] tab = table;;) {
Node<K,V> f;
}
JDK 1.8 ConcurrentHashMap 相较于 JDK 1.7 的优化
- 锁粒度更小,数据结构上抛弃了原来到 Segment 数组作为分段锁,采用 Node<K,V> []结构
- 整体采用 CAS 无锁并发,效率更高(插入时采用sychronized锁住了桶对象(即:单个链表))
- 多线程协助扩容,引入了标志位,当一个线程插入数据时发现正在扩容(hash = MOVED),不会阻塞等待扩容完成,而是检查是否可以协助其一起完成扩容操作(最少协助迁移16个槽位),这样既不会阻塞线程也达到了并发处理,加快迁移速度的效果
JDK 1.8 ConCurrentHashMap 源码分析:
重要成员变量
和HashMap一样有的成员变量(部分参数名不同)
-
DEFAULT_CAPACITY= 16; Hash表默认初始容量 16 -
MAXIMUM_CAPACITY = 1 << 30; 最大Hash表容量 230 -
LOAD_FACTOR = 0.75f;默认负载因子 -
TREEIFY_THRESHOLD = 8;链表转红黑树阈值 -
UNTREEIFY_THRESHOLD = 6;红黑树转链表阈值 -
MIN_TREEIFY_CAPACITY = 64;链表转红黑树时hash表最小容量阈值,达不到优先扩容
ConCurrentHashMap独有的种要成员变量
- MIN_TRANSFER_STRIDE = 16; table 扩容时, 每个线程最少迁移 table 的槽位个数
- MOVED = -1; 当 Node.hash 为 MOVED 时, 代表着 table 正在扩容
- TREEBIN= -2; 代表此元素后接红黑树
- Node<K,V>[] nextTable; table迁移过程临时变量, 迁移过程中将元素全部迁移到nextTable上
- int sizeCtl; 用来标志 table 初始化和扩容的,不同的取值代表着不同的含义:
-
-
-
0: table还没有被初始化 -
-1: table正在初始化 -
小于-1: 实际值为resizeStamp(n)<<RESIZE_STAMP_SHIFT+2, 表明table正在扩容 -
大于0: 初始化完成后, 代表 table 最大存放元素的个数, 默认为0.75 * n - int transferIndex: table 容量从 n 扩到 2n 时, 是从索引 n->1 的元素开始迁移, transferIndex 代表当前已经迁移的元素下标
- class ForwardingNode<K,V> extends Node<K,V>: 一个特殊的Node节点, 其hashcode=MOVED, 代表着此时table正在做扩容操作。扩容期间, 若table某个元素为null, 那么该元素设置为ForwardingNode, 当下个线程向这个元素插入数据时, 检查hashcode=MOVED, 就会帮着扩容
- 一个线程插完元素后, 检查 table 使用率, 若超过阈值, 调用 transfer() 进行扩容
- 一个线程插入数据时, 发现 table 对应元素的 hash = MOVED, 那么调用helpTransfer()协助扩容。
源码分析
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) {
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break;
}
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;
}
初始化Hash表
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();
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
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;
}
协助扩容,主要功能如下:
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) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
扩容transfer :新建一个nextTab, size是之前的2倍, 将table上的非空元素迁移到nextTab上面去
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;
if (nextTab == null) {
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
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;
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = 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 = (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;
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true;
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;
}
}
}
}
}
}
链表迁移原理:
- 遍历链表每个节点。 若节点的f.hash&n==0成立, 则将节点放在i, 否则, 则将节点放在n+i上面。
- 迁移前, 对该元素进行加锁。 遍历链表时, 这里使用lastRun变量, 保留的是上次hash的值, 假如整个链表全部节点f.hash&n==0, 那么第二次遍历, 只要找到lastRun的值, 那么认为之后的节点都是相同值, 减少了不必要的f.hash&n取值。遍历完所有的节点后, 此时形成了两条链表, ln存放的是f.hash&n=0的节点, hn存放的是非0的节点, 然后将ln存放在nextTable第i元素的位置, n+i存放在n+i的位置。
CopyOnWrite机制
问题:ArrayList 的 failfast 机制(失败快速重置机制):
在ArrayList在执行写操作(插入删除等)过程中,另一个线程去对它进行读操作,会破坏平衡,抛出异常(ConcurrentModificationException),是线程不安全的,所有 JAVA 引入了CopyOnWriteArrayList保证线程安全。
写多读少的情况下: 扩展ArrayList,重写add 方法加锁
读多写少,竞争不激烈情况下可以考虑:
CopyOnWrite 的核心思想: 读写分离,空间换时间,避免为保证并发安全导致的激烈锁竞争
适用:读多写少,竞争不激烈
特点:
- 读写分离
- 写加锁,读不加锁
- 只最终一致性,写过程中的更新等不会被读到
- 要保证其他线程及时读到最新数据可以使用volatile变量
CopyOnWriteArrayList 读写过程:
CopyOnWriteArrayList 源码分析:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
public E get(int index) {
return get(getArray(), index);
}
运算符
在阅读源码时可能遇到运算符不了解的情况,下面整理了一些常用的运算符:
二进制数在内存中是以补码的形式存放的(否则有符号运算时会发生异常)。正数的补码、反码都是其本身。负数的反码为符号位为1,其它位是原码取反。补码是符号位为1,其它位是原码取反,未位加1,即反码未位加 1
<< 表示左移。不分正负数,低位补 0,相当于乘 2n
>> 表示右移。如果该数为正,则高位补 0,若为负数,则高位补 1,相当于除 2n
>>> 表示无符号右移。也叫逻辑右移,即若该数为正,则高位补 0,而若该数为负数,则右移后高位同样补 0。正数时和 >> 结果相同。
& 表示按位与。只有两个数的二进制同时为1,结果才为1,否则为0。(负数按补码形式参加按位与运算),3 &5 即 00000011 & 00000101 = 00000001 ,所以 3 & 5的值为1
| 表示按位或。参加运算的两个数只要两个数中的一个为1,结果就为1,2 | 4 即 00000010 | 00000100 = 00000110 ,所以2 | 4的值为 6
~ 表示按位取反。~n = -(n+1),n+1就可以表示为 -~n
4
^ 表示按位异或。参加运算的两个数,如果两个相应位为值不同,则该位结果为1,否则为0, 2 ^ 4 即 00000010 ^ 00000100 =00000110 ,所以 2 ^ 4 的值为6
运算符优先级:
n |= n >>> 1 => n = n | n >>> 1 => n = n | (n >>> 1)
优先级 | 运算符 | 结合性 |
---|
1 | () [] . | 从左向右 | 2 | ! +(正) -(负) ~ ++ – | 从右向左 | 3 | * / % | 从左向右 | 4 | +(加) -(减) | 从左向右 | 5 | << >> >>> | 从左向右 | 6 | < <= > >= instanceof | 从左向右 | 7 | == != | 从左向右 | 8 | & | 从左向右 | 9 | ^ | 从左向右 | 10 | | | 从左向右 | 11 | && | 从左向右 | 12 | || | 从左向右 | 13 | ? : | 从右向左 | 14 | = += -= *= /= %= &= |= <>^= ~= <<= >>= >>>= | 从右向左 |
|