集合
非线程安全集合
1.List
(1)ArrayList
底层是数组(内存中分配是连续的内存空间)实现,数据的默认的长度是10,每当添加数据超过数据容量上限的时候就以复制的方式来进行动态扩容,扩容的数量是当前容量的1.5倍。
(2)LinkList
底层是循环双向链表实现,在内存中分配的空间是不连续的。LinkList数据结构由数据内容、前驱表项、后驱表项构成,为了可以快速定位到表头所以LinkList无论是否为空都会存在一个Head表项。
Array因为内存的连续性适合遍历但不适合插入和删除元素,Link适合删除插入元素,以为只需要改改变前后驱动指针,但不适合遍历。
2. Map
(1)HashMap
主体是由Entry的数组组成,每个Entry对象包含一个key-value键值对,同时还包含一个以Entry组成的链表,当HashMap在存储时遇到Hash碰撞就会使用链地址法用当前的数据的Entry作为链表在同一下标位置的数组内以链表的方式存储新值。每次新扩容会扩充为原来的2倍。
数据结构
- Entry集合:存放数据,其中本身可以组成量表解决哈希冲突;
- size:实际已经存储key-value键值对的个数;
- threshold:容积阈值,在没有指定初始化容积大小的情况下默认为capacity*loadFactory;
- loadFactor:负载因子,决定table填充度,默认为0.75,如果在默认容积16的情况下当被填充的table已经达到13时就会进行扩容;
- modCount:HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时,
如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),
需要抛出异常ConcurrentModificationException。
HashMap操作
初始化
在初始化时是可以指定HashMap的容积和负载因子的,在没有传入这两个参数的情况下使用默认16和0.75作为参数。
- 第一步,先判断初始容积是否0<initCapacity<MAX_CAPACITY(默认最大容量为2^29,如果初始化大于这个数也只会设置为最大数)。
- 第二步,将两个参数(initCapacity,loadFactor)指定为该HashMap参数并结束初始化。
HashMap在初始化时并不会分配table数组实际内存,只有在第一次put的时候才会实际分配内存。
Put
-
初始化内存(initflateTable),如果table为空默认第一次put会首先使用initCapacity初始化内存空间;
规范化容积一定要是2的次幂
-
获取Key散列值,首先对key获取hashCode,为了更加散列均匀HashMap会对key的hashcode再进行散列计算(计算出来的值很二进制下很可能大于容积最大长度);
-
计算下标,实际上就是与capacity进行位与操作(截取长度,位运算非常快);
-
存放数据(获取到Key),如果key为null会被存放在table[0]的位置或者table[0]的冲突链上,如果存在首先对key进行hash散列计算其在table实际位置。获取到该key的table数组中的Entry对象对比其中的数据或者其冲突链中的数据进行覆盖,如果获取不到key则在下面检查完容量再进行插入;
-
计数器加一,并发访问时快速返回失败。
-
插入未查询到key的数据,插入新数据先先检查容积的负载度,如果超过负载因子的阈值则先进行扩容再进行插入新的table数据。
Get
- 先判断是key是否为null,如果是去检索Table为0的Entry或者该处的冲突链;
- 如果不为null则根据Hash散列计算下标位置再去判断该位置数据以及其冲突链来找出具体值;
扩容
当进行put操作发现实际大小已经超过的HashMap的阈值的时候会调用resize来进行扩容:
- 检查旧table大小,如果已经已经等于MAX_CAPACITY则不再进行扩容只调整负载阈值,如果不大于最大容积则初始化容积,创建新Tabel;
- 遍历旧Table中所有数据与链表,重新计算每个Key的index放到新Table中;
实际上再扩容重新计算下标的时候并不是将所有旧Table中的数据全部都重新计算并修改其引用到新数组中,因为旧数据容积是2的次幂,所以在最后计算下标(indexFor)中h&(lenth-1)除了最高位全部都是1,再扩容重新计算的时候因为新Table的lenth-1相对于旧Table的lenth-1最高位只是多出一位1,那再重新计算h的index时,只要最高位不为0则计算出的index和远Table位置一样,可以大大减少之前已经散列好的数据位置。以减少扩容带来的成本。
在1.8对HashMap针对冲突链进行了优化,在冲突链的数量大于8的时候就会转换成红黑树(平衡二叉树)来存储冲突链。
Question:
- jdk1.8之前并发操作hashmap时为什么会有死循环的问题?
https://blog.csdn.net/qq_38157516/article/details/81024027
因为HashMap不是线程安全的所以两个线程在同时put相同元素且已经达到阈值时都会进行扩容,因为在扩容重新计算index的时候并不是修改其中数据而是修改其引用,所以两个线程同时修改时就会造成元素之间互相引用所以在下次查询到该Table值时就会造成死循环。
(2)LinkedHashMap
LinkedHashMap集成HashMap类,相对于HashMap无顺存储LinkHashMap是有序的(插入顺序,访问顺序)。其中访问顺序通过LRU(Least Recently Used)算法来实现访问热度的排序。
数据结构
LinkedHashMap集成了HashMap所以主要有Entry组成的数组构成,但是又另外维护了一份双向链表。该双向链表。相比于HashMap,LinkedHashMap的Entry多了after和before的作为额外的双向链表组成部分。
操作
初始化
无参数初始化,除了初始化HashMap的无参构造方法还设置LinkedHashMap的新参数accessOrder(排序方式),设置为true则存储排序方式为访问顺序否则为插入顺序。
初始化默认双向链表排序顺序
重写hashMap的init方法,实质初始化双向链表中的头部节点,使头部的节点的after和before指向自己。
Put
在LinkHashMap中存在两种排序访问与插入,在put的操作的时候也有不同:
插入顺序:如果对应Key不存在其Entry则进行双向链表的重排序,将链表的头的before的Entry指向头的after,再将新插入的Entry指向链表头的before,如果Entry
访问顺序:put,get等等所欲对于LinkHashMap的操作都会引起双向链表的重排序(删除双向链表中的该Entry再在header的before添加该Entry)
(3)TreeMap
TreeMap通过红黑树(自平衡二叉查找树)存储Key-Value键值,其继承NagivableMap结构实现了SortMap接口,可以支持自定义Key排序算法,对于默认自然顺序是指Key类新的compare方法。
数据结构
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
/**
* The comparator used to maintain order in this tree map, or
* null if it uses the natural ordering of its keys.
*
* @serial
*/
private final Comparator<? super K> comparator;
private transient Entry<K,V> root;
/**
* The number of entries in the tree
*/
private transient int size = 0;
/**
* The number of structural modifications to the tree.
*/
private transient int modCount = 0;
}
- comparator:排序规则默认自然顺序(自然顺序中Key不能为空)
- root:红黑树根节点
- size:红黑树中节点的个数,即Entry数量
- modCount:红黑树调整结构次数
TreeMap的Entry数据结构
构造方法
不指定comparator则使用自然顺序
操作
Put
- 判断是否存在根节点,不存在就创建根节点存在就开始遍历;
- 存在相同Key覆盖Value,不存在节点,增加查找到的最后节点作为父亲节点增加子节点;
- 调整红黑树节点颜色(自旋,变色)。
时间复杂度:O(log(N))
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
Get
通过二分法来查找指定的Key,因为Key的存储是使用红黑树,所以在查找的时候时间复杂度为O(N)。
(4)Set
Set的主要实现类HashSet底层数据结构为Hash表,在每次存放数据的时候都会hashcode和equals来确定是否是重复元素,是则不保存,不是则存入。如果对象的hashCode值是不同的,那么HashSet会认为对象是不可能相等的。如果hashCode相同会继续使用equals 进行比较.如果 equals为true 那么HashSet认为新加入的对象重复了,所以加入失败。如果equals 为false那么HashSet 认为新加入的对象没有重复.新元素可以存入。
线程安全集合
1.Vector
Vector继承于AbstractList,实现了List、RandomAccess、Cloneable、 Serializable等接口。实际上数据结构与ArrayList相同,实际是由数组构成。在Vector初始化时, 默认容量为10,扩容默认为原容量一倍。扩容时先复制到新数组再扩容。
其对数据的操作基本与ArrayList相同,但是在保证线程安全的同时,对于数组的操作使用了synchronized关键字,所以大大降低了效率。
2.Hashtable
同样实现了Map,底层也是由哈希表实现,但是相比于HashMap,对于HashTable是线程安全的,因为各个操作方法put,get等等都由synchronized修饰过,所以可以保证线程安全。保证线程安全就限制了不能像HashMap一样可以允许Key为NULL,Hashtable必须保证其Key为非空value。但是每次操作都是用synchronized修饰而且在解决哈希冲突的时候只使用链表进行解决,所以当数据量稍多后性能会出现大幅度降低。
3.ConcurrentHashMap
线程安全的HashMap,但又比HashTable效率高,其实质是将Entry构成的数据分段进行加锁,在1.7效率依然很低,因为还是很容易造成竞争一把锁而阻塞,1.8进行了优化不仅对数据结构上进行优化相同Entry在解决哈希冲突以及存储数据上进行了大幅度优化。
数据结构
JDK1.7
Segment数组和HashEntry数组构成,主要原理实现锁分离来实现线程安全。Segment数据将HashEntry组成的哈希表分成多个小块进行分开加锁以保证不造成大量竞争锁的现象。Segment数组大小默认16,每个Segment都会有一把锁保证线程安全。
JDK1.8
舍弃了Segment实现锁分离的思想,整体上使用HashMap数据结构数组+链表+红黑树和synchronized修饰方法来实现线程安全的,1.8对于sychronized锁的粒度进行了细化,大量减小了锁的竞争。
扩容
JDK1.7
外部使用synchorized来进行加锁,在每个segment内的HashTable进行扩容时,首先对该segment进行加锁,然后初始化出一个新的table数组,大小为原来的两倍,因为HashMap在进行hash函数寻址的时候最后要用数据容量大小-1二进制进行截取,所以在两层循环遍历中,某一个元素在table数组中的位置只存在两种结果(原位置,原位置+原数组大小)。因为外部已经加过锁所以在扩容的时候不会再出现因为使用头插法而导致并发扩容时造成的死循环引用。完成扩容后用新table数组替换就table数组的引用。
// 方法参数上的 node 是这次扩容后,需要添加到新的数组中的数据。
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
// 2 倍
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
// 创建新数组
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
// 新的掩码,如从 16 扩容到 32,那么 sizeMask 为 31,对应二进制 ‘000...00011111’
int sizeMask = newCapacity - 1;
// 遍历原数组,老套路,将原数组位置 i 处的链表拆分到 新数组位置 i 和 i+oldCap 两个位置
for (int i = 0; i < oldCapacity ; i++) {
// e 是链表的第一个元素
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
// 计算应该放置在新数组中的位置,
// 假设原数组长度为 16,e 在 oldTable[3] 处,那么 idx 只可能是 3 或者是 3 + 16 = 19
int idx = e.hash & sizeMask;
if (next == null) // 该位置处只有一个元素,那比较好办
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
// e 是链表表头
HashEntry<K,V> lastRun = e;
// idx 是当前链表的头结点 e 的新位置
int lastIdx = idx;
// 下面这个 for 循环会找到一个 lastRun 节点,这个节点之后的所有元素是将要放到一起的
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
// 将 lastRun 及其之后的所有节点组成的这个链表放到 lastIdx 这个位置
newTable[lastIdx] = lastRun;
// 下面的操作是处理 lastRun 之前的节点,
// 这些节点可能分配在另一个链表中,也可能分配到上面的那个链表中
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
// 将新来的 node 放到新数组中刚刚的 两个链表之一 的 头部
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
JDK1.8
1.8与1.7数据结构发生了很大的变化,1.8不再使用segment锁分离机制进行同步,而是使用synchronized按照table数组中的元素进行锁粒度细分进行多线程扩容。
- 通过cpu核心数和Map数组数量来计算桶数量,默认是16。(扩容线程可以有多个,每个扩容线程最处理16个桶)
- 初始化新数组nextTable;(每次扩容默认原来容量两倍)
- 死循环扩容;(finishing来控制是否扩容结束)
- 扩容线程先拿桶内数据区间进行数据转移默认是第i(16)个数组,扩容完该桶再i–移动到下一区间;(其中使用bound来标记该区间最小下标,如果小于最小下标则处理下一个桶或完成扩容。advance参数来判断是否推进下一个桶)
- 判断该数组槽位是否为空如果为空则CAS循环插入占位符,让putval方法感知,拒绝插入;
- 如果该槽位不为空但是槽位存放的是占位符,则认为有别的线程已经判断过该槽位,跳过该槽位;
- 如果该槽位不为空也不为占位符则处理数组内数据,进行数据转移;
!! 处理数据:- 使用synchronized对该数组节点进行加锁;
- 遍历节点数据进行迁移
(1). 如果是链表则头还是遍历,重新计算计算hash最后实际上只是两种结果(hash之后会按照数组的长度进行截取,所以重新计算不是在当前位置就是在就数组大小+当前位置),将每个元素遍历完后分为高位组和低位组分别以链表方式存放在数组中;
(2).如果是红黑树则像链表一样遍历所有树内元素分为两个数组在重新存放在nextTable中,但是重新存放时如果组内元素大于6,则还是按照红黑树进行存储;
- 扩容线程先拿桶内数据区间进行数据转移默认是第i(16)个数组,扩容完该桶再i–移动到下一区间;(其中使用bound来标记该区间最小下标,如果小于最小下标则处理下一个桶或完成扩容。advance参数来判断是否推进下一个桶)
- 结束循环
- 判断是否结束扩容
- 结束:清理临时变量,重新计算阈值,更新table完成扩容。;
- 未结束:未检测到完成但是无法领取下一个桶则将sizeCtl减一代表当前扩容线程退出;
- 判断是否结束扩容
/**
* Moves and/or copies the nodes in each bin to new table. See
* above for explanation.
*
* transferIndex 表示转移时的下标,初始为扩容前的 length。
*
* 我们假设长度是 32
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 将 length / 8 然后除以 CPU核心数。如果得到的结果小于 16,那么就使用 16。
// 这里的目的是让每个 CPU 处理的桶一样多,避免出现转移任务不均匀的现象,如果桶较少的话,默认一个 CPU(一个线程)处理 16 个桶
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range 细分范围 stridea:TODO
// 新的 table 尚未初始化
if (nextTab == null) { // initiating
try {
// 扩容 2 倍
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
// 更新
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
// 扩容失败, sizeCtl 使用 int 最大值。
sizeCtl = Integer.MAX_VALUE;
return;// 结束
}
// 更新成员变量
nextTable = nextTab;
// 更新转移下标,就是 老的 tab 的 length
transferIndex = n;
}
// 新 tab 的 length
int nextn = nextTab.length;
// 创建一个 fwd 节点,用于占位。当别的线程发现这个槽位中是 fwd 类型的节点,则跳过这个节点。
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 首次推进为 true,如果等于 true,说明需要再次推进一个下标(i--),反之,如果是 false,那么就不能推进下标,需要将当前的下标处理完毕才能继续推进
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 递减。同时,每个线程都会进入这里取得自己需要转移的桶的区间
while (advance) {
int nextIndex, nextBound;
// 对 i 减一,判断是否大于等于 bound (正常情况下,如果大于 bound 不成立,说明该线程上次领取的任务已经完成了。那么,需要在下面继续领取任务)
// 如果对 i 减一大于等于 bound(还需要继续做任务),或者完成了,修改推进状态为 false,不能推进了。任务成功后修改推进状态为 true。
// 通常,第一次进入循环,i-- 这个判断会无法通过,从而走下面的 nextIndex 赋值操作(获取最新的转移下标)。其余情况都是:如果可以推进,将 i 减一,然后修改成不可推进。如果 i 对应的桶处理成功了,改成可以推进。
if (--i >= bound || finishing)
advance = false;// 这里设置 false,是为了防止在没有成功处理一个桶的情况下却进行了推进
// 这里的目的是:1. 当一个线程进入时,会选取最新的转移下标。2. 当一个线程处理完自己的区间时,如果还有剩余区间的没有别的线程处理。再次获取区间。
else if ((nextIndex = transferIndex) <= 0) {
// 如果小于等于0,说明没有区间了 ,i 改成 -1,推进状态变成 false,不再推进,表示,扩容结束了,当前线程可以退出了
// 这个 -1 会在下面的 if 块里判断,从而进入完成状态判断
i = -1;
advance = false;// 这里设置 false,是为了防止在没有成功处理一个桶的情况下却进行了推进
}// CAS 修改 transferIndex,即 length - 区间值,留下剩余的区间值供后面的线程使用
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;// 这个值就是当前线程可以处理的最小当前区间最小下标
i = nextIndex - 1; // 初次对i 赋值,这个就是当前线程可以处理的当前区间的最大下标
advance = false; // 这里设置 false,是为了防止在没有成功处理一个桶的情况下却进行了推进,这样对导致漏掉某个桶。下面的 if (tabAt(tab, i) == f) 判断会出现这样的情况。
}
}// 如果 i 小于0 (不在 tab 下标内,按照上面的判断,领取最后一段区间的线程扩容结束)
// 如果 i >= tab.length(不知道为什么这么判断)
// 如果 i + tab.length >= nextTable.length (不知道为什么这么判断)
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { // 如果完成了扩容
nextTable = null;// 删除成员变量
table = nextTab;// 更新 table
sizeCtl = (n << 1) - (n >>> 1); // 更新阈值
return;// 结束方法。
}// 如果没完成
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {// 尝试将 sc -1. 表示这个线程结束帮助扩容了,将 sc 的低 16 位减一。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)// 如果 sc - 2 不等于标识符左移 16 位。如果他们相等了,说明没有线程在帮助他们扩容了。也就是说,扩容结束了。
return;// 不相等,说明没结束,当前线程结束方法。
finishing = advance = true;// 如果相等,扩容结束了,更新 finising 变量
i = n; // 再次循环检查一下整张表
}
}
else if ((f = tabAt(tab, i)) == null) // 获取老 tab i 下标位置的变量,如果是 null,就使用 fwd 占位。
advance = casTabAt(tab, i, null, fwd);// 如果成功写入 fwd 占位,再次推进一个下标
else if ((fh = f.hash) == MOVED)// 如果不是 null 且 hash 值是 MOVED。
advance = true; // already processed // 说明别的线程已经处理过了,再次推进一个下标
else {// 到这里,说明这个位置有实际值了,且不是占位符。对这个节点上锁。为什么上锁,防止 putVal 的时候向链表插入数据
synchronized (f) { //扩容时,只在这个环节加锁
// 判断 i 下标处的桶节点是否和 f 相同
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;// low, height 高位桶,低位桶
// 如果 f 的 hash 值大于 0 。TreeBin 的 hash 是 -2
if (fh >= 0) {
// 对老长度进行与运算(第一个操作数的的第n位于第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0)
// 由于 Map 的长度都是 2 的次方(000001000 这类的数字),那么取于 length 只有 2 种结果,一种是 0,一种是1
// 如果是结果是0 ,Doug Lea 将其放在低位,反之放在高位,目的是将链表重新 hash,放到对应的位置上,让新的取于算法能够击中他。
int runBit = fh & n;
Node<K,V> lastRun = f; // 尾节点,且和头节点的 hash 值取于不相等
// 遍历这个桶
for (Node<K,V> p = f.next; p != null; p = p.next) {
// 取于桶中每个节点的 hash 值
int b = p.hash & n;
// 如果节点的 hash 值和首节点的 hash 值取于结果不同
if (b != runBit) {
runBit = b; // 更新 runBit,用于下面判断 lastRun 该赋值给 ln 还是 hn。
lastRun = p; // 这个 lastRun 保证后面的节点与自己的取于值相同,避免后面没有必要的循环
}
}
if (runBit == 0) {// 如果最后更新的 runBit 是 0 ,设置低位节点
ln = lastRun;
hn = null;
}
else {
hn = lastRun; // 如果最后更新的 runBit 是 1, 设置高位节点
ln = null;
}// 再次循环,生成两个链表,lastRun 作为停止条件,这样就是避免无谓的循环(lastRun 后面都是相同的取于结果)
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
// 如果与运算结果是 0,那么就还在低位
if ((ph & n) == 0) // 如果是0 ,那么创建低位节点
ln = new Node<K,V>(ph, pk, pv, ln);
else // 1 则创建高位
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 其实这里类似 hashMap
// 设置低位链表放在新链表的 i
setTabAt(nextTab, i, ln);
// 设置高位链表,在原有长度上加 n
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);
// 和链表相同的判断,与运算 == 0 的放在低位
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
} // 不是 0 的放在高位
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 如果树的节点数小于等于 6,那么转成链表,反之,创建一个新的树
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;
}
}
}
}
}
}
PS:这篇文章很有启示
4.CopyOnWriteArrayList
其实现是使用“写时复制”思想的List,只有在进行更新操作的时候调用者会新建一份线程专属的List的副本,所有修改操作都在该副本上进行操作,当修改完成后再将副本进行覆盖主内存中List数据。当副本产生是CopyOnWriteArrayList中的ReentrantLock(重入锁)就会排斥其他写锁防止产生多份副本,而在副本进行修改的时候其他线程读取的都是主内存中的原始的ArrayList数据,只有在释放写锁进行覆盖后才会读取到新数据。
数据结构
由重入锁lock和volatile修饰的数组构成,因为使用volatile(改时通知)修饰,所以在使用副本替换主内存中的数据是,替换完成后每个使用该变量的线程都会收到修改的通知从而去主内存中刷新数据。在更新时因为使用重铸锁所以可以实现线程同步,但是在数据较大时每次制造副本和刷新数据的时候会造成频繁的GC。
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** The lock protecting all mutators */
//重入锁 读读共享、写写互斥、读写互斥、写读互斥
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
//数组
private transient volatile Object[] array;
操作
add,remove等等所有操作都会使用该List对象的重入锁进行加锁,用来确认线程安全
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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(); // 释放锁
}
}
5.BlockingQueue
阻塞队列,底层还是一个队列数据先进先出后进后出。
ArrayBlockingQueue
线程安全主要有队列内ReentrantLock 和Condition实现,同时只能有一个线程可以进行队列的出或入操作。
数据结构
基于数组的阻塞队列实现,底层是由一个数组、入队出队两个Condition、ReentrantLock、头尾数组指标和总数指标构成
数组(items):实际存放数据,一单被创建出来不能进行扩容或者缩减,当容器满后入队操作线程进入阻塞状态。
notEmpyt(出队监视器):阻塞队列,当出队操作线程出现阻塞后,所有被阻塞的线程进入到该监视器中进行控制。
notFull(入队监视器): 当数组容量满后还存在入队操作的线程,则线程都进入该监视器中进行管理。
takeIndex(队列头index):阻塞队列头部下标
putINdex(队列尾index):阻塞队列尾部下标
count:队列元素总数
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
/**
* Serialization ID. This class relies on default serialization
* even for the items array, which is default-serialized, even if
* it is empty. Otherwise it could not be declared final, which is
* necessary here.
*/
private static final long serialVersionUID = -817911632652898426L;
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
/** Number of elements in the queue */
int count;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
/**
* Shared state for currently active iterators, or null if there
* are known not to be any. Allows queue operations to update
* iterator state.
*/
transient Itrs itrs = null;
操作
put
首先检查元素是否为空,能被入队的元素不能为空,其次检查队列是否已满?如果数组已满则会调用notFull的await释放锁并处于阻塞状态。
ublic void put(E e) throws InterruptedException {
//检查元素是否为null,如果是,抛出NullPointerException
checkNotNull(e);
final ReentrantLock lock = this.lock;
//加锁
lock.lockInterruptibly();
try {
//如果队列已满,阻塞,等待队列成为不满状态
while (count == items.length)
notFull.await();
//将元素入队
enqueue(e);
} finally {
lock.unlock();
}
}
take
获取ReentrantLock后,如果队列为空,则进入阻塞,知道队列内有元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//首先加锁
lock.lockInterruptibly();
try {
//如果队列为空,阻塞
while (count == 0)
notEmpty.await();
//队列不为空,调用dequeue()出队
return dequeue();
} finally {
//释放锁
lock.unlock();
}
}
LinkBlockingQueue
底层是由数组构成,所以其中数据结构中维护了头尾两个Node(head数据始终为null),还有两把锁用来维护出入队操作以及所关联的Condition(等待队列)。除了这些还有维护了两个变量一个存储当前队列存量,一个定义队列容量,因为LinkBlockingQueue队列的容量是可变的。因为出入队在实现线程安全时维护的是两把分开的锁所以该队列允许同时存在出入队两个线程操作,但是不允许存在相同操作的两个线程。其中维护当前队列存量因为要保证两个线程存取时的线程安全自身变量操作时原子性的类(AtomicInteger)。
操作
Put
- 新建一个新的节点;
- 获取入队锁;
- 检查存量是否已满“
- 未满:将存量加一并将新节点放至队尾;
- 已满:该入队线程进入入队等待对接,状态改为阻塞;
- 检查是否还有空余容量释放其他入队等待线程;
- 解锁
- 通知出队等待队列线程可以出队操作
public void put(E e)throws InterruptedException {
//不允许元素为null
if (e ==null)
throw new NullPointerException();
int c = -1;
//以当前元素新建一个节点
Node node =new Node(e);
final ReentrantLock putLock =this.putLock;
final AtomicInteger count =this.count;
//获得入队的锁
putLock.lockInterruptibly();
try {
//如果队列已满,那么将该线程加入到Condition的等待队列中
while (count.get() == capacity) {
notFull.await();
}
//将节点入队
enqueue(node);
//得到插入之前队列的元素个数
c = count.getAndIncrement();
//如果还可以插入元素,那么释放等待的入队线程
if (c +1 < capacity){
notFull.signal();
}
}finally {
//解锁
putLock.unlock();
}
//通知出队线程队列非空
if (c ==0)
signalNotEmpty();
}
take
- 取队列头部的下一个元素;
- 队列为空,就加入到notEmpty(的条件等待队列中;
- 当队列不为空时就取走一个元素,当取完发现还有元素可取时,再通知一下自己的伙伴(等待在条件队列中的线程),最后,如果队列从满到非满,通知一下put线程。
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//获取takeLock锁
takeLock.lockInterruptibly();
try {
//如果队列为空,那么加入到notEmpty条件的等待队列中
while (count.get() == 0) {
notEmpty.await();
}
//得到队头元素
x = dequeue();
//得到取走一个元素之前队列的元素个数
c = count.getAndDecrement();
//如果队列中还有数据可取,释放notEmpty条件等待队列中的第一个线程
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
//如果队列中的元素从满到非满,通知put线程
if (c == capacity)
signalNotFull();
return x;
}
remove
相比于put和take操作,remove在操作时需要同时获取头尾两把锁然后遍历队列删除指定的node。完成删除后不需要通知入队或者出队队列。
public boolean remove(Object o) {
//因为队列不包含null元素,返回false
if (o == null) return false;
//获取两把锁 fullyLock();
try {
//从头的下一个节点开始遍历
for (Node trail = head, p = trail.next;
p != null;
trail = p, p = p.next) {
//如果匹配,那么将节点从队列中移除,trail表示前驱节点
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
return false;
} finally {
//释放两把锁
fullyUnlock();
}
}
DelayQueue
无界阻塞延迟队列,队列中每个元素都要实现Delayed接口,其中Delayed中的getDelay方法和compareTo方法用来设置元素剩余时限和对元素进行排序保证队尾是过期时间最小的元素(如果comareTo写的不当可能会造成已超时元素堆积但是队尾却是未超时的元素)
数据结构
public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> {
//全局锁,用于实现线程安全
private final transient ReentrantLock lock = new ReentrantLock();
//优先队列,存放元素
private final PriorityQueue<E> q = new PriorityQueue<E>();//元素存放
//leader线程,实际上每次进行入队和出队操作的只能是leader线程,其余的都叫做fallower线程,这里会用到一个leader-follower模式
// leader是等待获取队列头元素的线程,主从式设计减少不必要的等待。
// 如果leader != null,表示已经有线程在等待获取队列的头元素,会通过await()方法让出当前线程等待信号。
// q如果leader==null,则把当前线程设置为leader,当一个线程为leader时,会使用awaitNanos()让当前线程等待接受信号,或等待delay时间。
private Thread leader = null;
//线程阻塞队列
private final Condition available = lock.newCondition();
}
操作
offer
- 获取全局锁
- 向优先队列插入元素
- 唤醒线程阻塞队列内的线程
- 释放锁
public boolean offer(E e) {
// 获取全局独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 向优先队列中插入元素
q.offer(e);
// 如果队首元素是刚插入的元素,则设置leader为null,并唤醒阻塞在available上的线程
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
// 释放全局独占锁
lock.unlock();
}
}
take 阻塞式出队
- 加全局锁
- 判断队首元素是否为空
- 为空:将该线程放进线程阻塞队列等待元素插入
- 不为空:插入队列流程
- 将该线程设置为leader线程,表示已经有线程正在操作
- 判断首个元素是否超时没超时进入阻塞队列等待超时
- 超时取出元素
- leader线程置空
- 通如果没有线程继续操作且首元素不为空则通知阻塞队列内其他线程进行操作
- 释放全局锁
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return q.poll();
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
pop 非阻塞式出队
- 加锁,全局锁
- 获取首元素
- 为空或者还没超时返回null
- 不为空且已经超时返回元素
- 释放锁
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
lock.unlock();
}
}
SynchronousQueue
SynchronousQueue是一个内部只能包含一个元素的队列。插入队列的线程在插入时会阻塞到直到另一个线程来获取元素。如果获取元素的线程在容量为空时同样会阻塞直到另一个插入队列插入元素,所以SynchronousQueue队列更像一个同步的点,其中存在两种队列stack,queue来进行公平和非公平队列的等待。
数据结构
其中主要有两个类TransferStack和TransferQueue。其中TransferStack使用LIFO顺序存储元素,这个类用于非公平的模式,TransferQueue使用FIFI顺序存储元素,这个类用于公平的模式。
https://www.jianshu.com/p/d5e2e3513ba3