1.5 集合
最顶层 是 collection ===》 用于定义集合的操作
第二层 queue list set
Map接口 是所有 map 的顶级接口
1.5.1 快速失败机制
快速失败(fail—fast) 是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
他的原理是啥? CAS
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 expectedModCount 变量。 开始时候 expectedModCount = modCount=N
集合在被遍历期间如果内容发生变化,就会改变modCount的值。 如果其他线程使用 非 iteratrior 发生修改 modCount 会+1
每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
只有使用到了 Iterator 进行遍历的时候才会出现. foreach 也是,如果是使用 for 不会出现
1.5.2 安全失败
由于迭代时是对原集合的拷贝进行遍历
,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会ConcurrentModificationException 异常
1.5.1 ArrayLsit
// 默认大小为10 ,创建一个 final 修饰的 object数组
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
安全问题
Arraylsit
线程不安全
vector
线程安全,性能最差,所有方法采用 synchronized
SynchronizedList
和vector 差不多
CopyOnWriteArrayList
它的添加时加锁的(ReentrantLock ,非synchronized同步锁),读操作是没有加锁。每一次写操作 都是通过 array.copy()创建新的数组,读取是通过副本,所以读取存在一定的滞后性
扩容机制
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
//如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
//从上面 grow() 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
//对minCapacity和MAX_ARRAY_SIZE进行比较
//若minCapacity大,将Integer.MAX_VALUE作为新数组的大小
//若MAX_ARRAY_SIZE大,将MAX_ARRAY_SIZE作为新数组的大小
//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
// 新产生的容量 大于 MAX_ARRAY_SIZ
// newCapacity > MAX_ARRAY_SIZ
// minCapacity 小于 Integer.MAx_VALUE - 8 的时候, 会采用 MAX_ARRAY_SIZE 来作为大小
// minCapacity 大于 Integer.MAx_VALUE - 8 的时候, 会自动采用 Integer.MAX_VALUE 作为最大值
// minCapacity 大于 Integer.MAx_VALUE 时候 报错
// Arrays.copyOf 通过创建一个数组进行返回
// 通过唯一运算进行扩容操作
int newCapacity = oldCapacity + (oldCapacity >> 1);
注意,存在三个容量:newcapacity
、mincapacity
、oldcapacity
总结下:
-
如果新产生的 newcapacity <于 mincapacity (这种情况是一次加入多个数据时候,单次扩容无法容纳的情况) newcapacity 会等于 mincapacity
-
如果newcapacity 大于 MAX_ARRAY_SIZE 的时候,会去比较 mincapacity 和 MAX_ARRAY_SIZE的值
- minCapacity 小于 Integer.MAx_VALUE - 8 的时候, 会采用 MAX_ARRAY_SIZE 来作为大小
- minCapacity 大于 Integer.MAx_VALUE - 8 的时候, 会自动采用 Integer.MAX_VALUE 作为最大值
- minCapacity 大于 Integer.MAx_VALUE(小于 0 ) 时候 报错
Linked 和 Array 的对比
记住底层一个是数组,一个是链表就差不多了
1.5.2 Map
(1)HashMap(这里以jdk8)
- 线程不安全
- 可以存储 null 的 key 和 value
- 初始大小 16 扩容时候 是两倍
- 底层数据结构 数组,链表,红黑树
如何保证HashMap的大小都是 2的 n此方
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
这样子做的好处:
扩容的时候,需要改变的位置,有些不需要调换,有一些值需要扩大原来的一倍
hash 值的求法
hashcode 的高16位和低16位进行异或操作
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:⽆符号右移,忽略符号位,空位都以0补⻬ =====》 000 110
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
无符号 移动 >> 有符号移动
头插法和尾插法
A.因为头插法会造成死链,参考链接
B.JDK7用头插是考虑到了一个所谓的热点数据的点(新插入的数据可能会更早用到),但这其实是个伪命题,因为JDK7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置(就是因为头插) 所以最后的结果 还是打乱了插入的顺序 所以总的来看支撑JDK7使用头插的这点原因也不足以支撑下去了 所以就干脆换成尾插 一举多得
插入
// 参数onlyIfAbsent表示是否替换原值
// 参数evict我们可以忽略它,它主要用来区别通过put添加还是创建时初始化数据的
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)
// resize()不仅用来调整大小,还用来进行初始化配置
n = (tab = resize()).length;
// (n - 1) & hash这种方式也熟悉了吧?都在分析ArrayDeque中有体现
//这里就是看下在hash位置有没有元素,实际位置是hash % (length-1)
if ((p = tab[i = (n - 1) & hash]) == null)
// 将元素直接插进去
tab[i] = newNode(hash, key, value, null);
else {
//这时就需要链表或红黑树了
// e是用来查看是不是待插入的元素已经有了,有就替换
Node<K,V> e; K k;
// p是存储在当前位置的元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //要插入的元素就是p,这说明目的是修改值
// 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);
// 链表比较长,需要树化,
// 由于初始即为p.next,所以当插入第8个元素才会树化
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;
}
}
// 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太大,达到了capacity的0.75,需要扩容
if (++size > threshold)
resize();
// 默认也是空实现,允许我们插入完成后做一些操作
afterNodeInsertion(evict);
return null;
}
首先将新的节点添加到链表的尾巴中,然后判断链表中的长度是否大于7(因为加上最新的这个节点,链表的长度就是8),然后转为红黑树
扩容
如果是单个元素,先把数组下标的值设为null,方便jvm回收,然后根据e.hash & (newCap - 1)算出新数组的下标位置,直接迁移到新数组。
单链表结构则会将原来的单链表有可能分割成两个单链表,一个记为低位链表loHead、另一个记为高位链表hiHead,最后分别插入到原数组的j下标处、以及j+oldCap处,这里采用的是修改引用的方法并没有复制出来新的节点!!在concurrentHashMap中采用的是复制出来新的节点,用于实现扩容时候读取
如果是红黑树,会转为单链表在进行操作(split方式)
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//首次初始化后table为Null
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;//默认构造器的情况下为0
int newCap, newThr = 0;
if (oldCap > 0) {//table扩容过
//当前table容量大于最大值得时候返回当前table
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//table的容量乘以2,threshold的值也乘以2
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//使用带有初始容量的构造器时,table容量为初始化得到的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) {
HashMap.Node<K,V> e;
if ((e = oldTab[j]) != null) {
// help gc
oldTab[j] = null;
if (e.next == null)
// 当前index没有发生hash冲突,直接对2取模,即移位运算hash &(2^n -1)
// 扩容都是按照2的幂次方扩容,因此newCap = 2^n
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof HashMap.TreeNode)
// 当前index对应的节点为红黑树,这里篇幅比较长且需要了解其数据结构跟算法,因此不进行详解,当树的个数小于等于UNTREEIFY_THRESHOLD则转成链表
((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 把当前index对应的链表分成两个链表,减少扩容的迁移量
HashMap.Node<K,V> loHead = null, loTail = null;
HashMap.Node<K,V> hiHead = null, hiTail = null;
HashMap.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) {
// help gc
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
// help gc
hiTail.next = null;
// 扩容长度为当前index位置+旧的容量
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
(2)HashTable
-
线程安全
-
不可以存储 null 的 key 和 value
-
默认大小是11,每次扩充是其 2*n+1
-
通过锁住整张表保证安全
(3)ConcurrentHashMap
采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
注意存在几种不同的节点
PUT操作
ConcurrentHashMap
在进行put操作的还是比较复杂的,大致可以分为以下步骤:
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
- 如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容,当前线程会辅助扩容,。 - 如果都不满足,则利用
synchronized
锁写入数据,说明是红黑树或者是链表或者是有存在数据。 - 如果数量大于
TREEIFY_THRESHOLD
则要转换为红黑树。
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(); // lazy Initialization
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 当前bucket为空
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) // 当前Map在扩容,先协助扩容,在更新值。
tab = helpTransfer(tab, f);
else { // hash冲突
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) //链表节点超过了8,链表转为红黑树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); // 统计节点个数,检查是否需要resize
return null;
}
get操作
首先:根据key的hash值计算映射到table的哪个桶,table[i];
其次:如果table[i]的key和待查找key相同,那直接返回(这时候不用判断是不是特殊节点);
最后:如果table[i]对应的结点是特殊结点红黑树节点或者是 fwd节点!! ,则通过 find() 查找,如果不是特殊节点,则按链表查找;
/**
* 根据key查找对应的value值
* @return 查找不到则返回null
* @throws NullPointerException 如果key为空,则抛出异常
*/
public V get(Object key) {
Node<K, V>[] tab;
Node<K, V> e, p;
int n, eh;
K ek;
int h = spread(key.hashCode()); // 重新计算key的hash值
//(e = tabAt(tab, (n - 1) & h)) != null找到对应的桶
if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
//CASE1:看table[i]当前节点是不是要找的值
if ((eh = e.hash) == h) { // table[i]就是待查找的桶,直接找值
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
//CASE2:当前节点不是,则判断是不是特殊节点,如果是find查找
} else if (eh < 0) // hash值 < 0 , 说明遇到特殊结点(非链表结点), 调用find方法查找
return (p = e.find(h, key)) != null ? p.val : null;
//CASE3:当前不是特殊节点,则链表遍历查找
while ((e = e.next) != null) { // 遍历table[i]中的结点,因为前面通过eh判断过不是特殊节点了,则直接按链表查找
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
扩容操作
在扩容过程中同时支持get查数据,若有线程put数据,还会帮助一起扩容,代码比较难懂
大概总结下,有这几个关键点
-
put线程辅助扩容,会根据cpu核数,去分配每个线程负责的桶
-
扩容时候采用的是复制出来节点的方法,所以至此读取操作
-
扩容的时候,对于链表会遍历两次,第一次找到 lastrun节点,而后采用头插法进行插入,最后进行转移
-
红黑树会转为链表进行遍历,然后根据高低位组装成为两个链表,最后判断是否需要变成树
/**
* 数据转移和扩容.
* 每个调用tranfer的线程会对当前旧table中[transferIndex-stride, transferIndex-1]位置的结点进行迁移
*
* @param tab 旧table数组
* @param nextTab 新table数组
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// stride可理解成“步长”,即“数据迁移”时,每个线程要负责旧table中的多少个桶,根据几核的CPU决定“步长”
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) //MIN_TRANSFER_STRIDE默认16
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // 第二个参数,nextable为null说明第一次扩容
try {
@SuppressWarnings("unchecked")
// 创建新table数组,扩大一倍,32,n还为16
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // 处理内存溢出(OOME)的情况
sizeCtl = Integer.MAX_VALUE; //将表示容量的sizeCtl 设置为最大值,然后返回
return;
}
nextTable = nextTab; //设置nextTable变量为扩容后的数组
transferIndex = n; // [transferIndex-stride, transferIndex-1]:表示当前线程要进行数据迁移的桶区间
}
int nextn = nextTab.length;
// ForwardingNode结点,当旧table的某个桶中的所有结点都迁移完后,用该结点占据这个桶
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 标识一个桶的迁移工作是否完成,advance == true 表示可以进行下一个位置的迁移
boolean advance = true;
// 最后一个数据迁移的线程将该值置为true,并进行本轮扩容的收尾工作
boolean finishing = false; // to ensure sweep before committing nextTab
// i标识桶索引, bound标识边界
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 每一次自旋前的预处理,主要是为了定位本轮处理的桶区间
// 正常情况下,预处理完成后:i == transferIndex-1:右边界;bound == transferIndex-stride:左边界
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSetInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// CASE1:当前是处理最后一个tranfer任务的线程或出现扩容冲突
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { // 所有桶迁移均已完成
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 扩容线程数减1,表示当前线程已完成自己的transfer任务
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 判断当前线程是否是本轮扩容中的最后一个线程,如果不是,则直接退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
/**
* 最后一个数据迁移线程要重新检查一次旧table中的所有桶,看是否都被正确迁移到新table了:
* ①正常情况下,重新检查时,旧table的所有桶都应该是ForwardingNode;
* ②特殊情况下,比如扩容冲突(多个线程申请到了同一个transfer任务),此时当前线程领取的任务会作废,那么最后检查时,
* 还要处理因为作废而没有被迁移的桶,把它们正确迁移到新table中
*/
i = n; // recheck before commit
}
}
// CASE2:旧桶本身为null,不用迁移,直接尝试放一个ForwardingNode
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// CASE3:该旧桶已经迁移完成,直接跳过
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
// CASE4:该旧桶未迁移完成,进行数据迁移
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// CASE4.1:桶的hash>0,说明是链表迁移
if (fh >= 0) {
/**
* 下面的过程会将旧桶中的链表分成两部分:ln链和hn链
* ln链会插入到新table的槽i中,hn链会插入到新table的槽i+n中
*/
int runBit = fh & n; // 由于n是2的幂次,所以runBit要么是0,要么高位是1
Node<K,V> lastRun = f; // lastRun指向最后一个相邻runBit不同的结点
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;
}
// 以lastRun所指向的结点为分界,将链表拆成2个子链表ln、hn
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); // ln链表存入新桶的索引i位置
setTabAt(nextTab, i + n, hn); // hn链表存入新桶的索引i+n位置
setTabAt(tab, i, fwd); // 设置ForwardingNode占位
advance = true; // 表示当前旧桶的结点已迁移完毕
}
// CASE4.2:红黑树迁移
else if (f instanceof TreeBin) {
/**
* 下面的过程会先以链表方式遍历,复制所有结点,然后根据高低位组装成两个链表;
* 然后看下是否需要进行红黑树转换,最后放到新table对应的桶中
*/
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); // 设置ForwardingNode占位
advance = true; // 表示当前旧桶的结点已迁移完毕
}
else if (f instanceof ReservationNode) //jdk16特有,1.8没有
throw new IllegalStateException("Recursive update");
}
}
}
}
}
第四篇讲的最细的一篇
扩容时机
(1) 元素个数达到扩容阈值。
(2) 调用 putAll 方法,但目前容量不足以存放所有元素时。
(3) 某条链表长度达到8,但数组长度却小于64时。
单线程情况下
多线程如何分配任务?
普通链表如何迁移?
红黑树如何迁移?
*hash桶迁移中以及迁移后如何处理存取请求?*
*多线程迁移任务完成后的操作*
第一个for循环:找到lastrun 两个if:la
strun节点变为高低位链的初始节点 第二个for循环:采用头插法往 lastRun 前面插,遍历到 lastRun 结束。
(4) TreeMap
红黑树 二叉平衡(待补充)
1.5.3 Queue
-
BolckingQueue
主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。
Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。
1.5.3.1 ArrayBlockingQueue和LinkedBlockingQueue
1. ArrayBlockingQueue采用数组进行存储,需要预先指定数组容量,对JVM比较友好,不需要后面JVM为其动态扩展其容量。LinkedBlockingQueue采用链表进行存储,储存元素的时候需要额外构造出Node节点,不需要预先指定链表大小,默认为Integer.MAX_VALUE,后面JVM需要为其动态的扩展内存空间
2. ArrayBlockingQueue的入队和出队操作都采用一把ReentrantLock锁去控制,而LinkedBlockingQueue的入队和出队操作采用两把ReentrantLock锁去控制,入队和出队操作不互斥
3. 在具有大量的出队和入队操作的场景下,ArrayBlockingQueue的性能要低于LinkedBlockingQueue的性能,就第2点讲的,ArrayBlockingQueue的出队和入队是互斥的,而LinkedBlockingQueue可以一边出队一边入队