并发集合类Collections
并发集合类概述
由于Collections集合类中支持并发的集合有很很多。这里只是提取其中比较常见且重要的集合进行讲解。本文主要从LinkedBlockingQueue、CopyOnWriteArrayList、ConcurrentHashMap三个集合类进行阐述。
LinkedBlockingQueue
LinkedBlockingQueue是在JDK1.5时,随着J.U.C包引入的一种阻塞队列,它实现了BlockingQueue接口,底层基于单链表实现。LinkedBlockingQueue是一种近似有界阻塞队列,为什么说近似?因为LinkedBlockingQueue既可以在初始构造时就指定队列的容量,也可以不指定,如果不指定,它的容量大小默认为Integer.MAX_VALUE。
它维护了两把锁——takeLock和putLock。
takeLock用于控制出队的并发,putLock用于入队的并发。这也就意味着,同一时刻,只能只有一个线程能执行入队/出队操作,其余入队/出队线程会被阻塞;但是,入队和出队之间可以并发执行,即同一时刻,可以同时有一个线程进行入队,另一个线程进行出队,这样就可以提升吞吐量。
LinkedBlockingQueue原理
下边从几种常用的方法的源码分析来学习一下LinkedBlockingQueue的内部原理。
LinkedBlockingQueue的构造函数
//无参构造,默认阻塞队列的容量大小为 Integer.MAX_VALUE
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
//指定容量
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
//从已有的集合构造队列,队列的容量为Integer.MAX_VALUE
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE); //指定容量大小
final ReentrantLock putLock = this.putLock;
putLock.lock(); // 这里加锁仅仅只是为了保证可见性
try {
int n = 0;
for (E e : c) {
if (e == null) //队列不能包含null元素
throw new NullPointerException();
if (n == capacity) //如果队列已经满了,抛出异常
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e)); //队尾插入元素
++n;
}
//private final AtomicInteger count = new AtomicInteger();
count.set(n); //这里使用一个 原子变量来表示队列中元素的个数。
} finally {
putLock.unlock();
}
}
根据以上构造函数可知,LinkedBlockingQueue如果不指定容量的大小,则默认的容量大小为Integer.MAX_VALUE。在构造函数LinkedBlockingQueue(Collection<? extends E> c)中,使用原子变量count来 保证出队/入队并发修改元素时数据的一致性。
LinkedBlockingQueue初始化之后,结构如下所示:
LinkedBlockingQueue的常用方法
由于接口和ArrayBlockingQueue完全一样,所以LinkedBlockingQueue会阻塞线程的方法也一共有4个:put(E e)、offer(E e)、add(E e)和take()、poll(),remove(Object o)我们从源码分析分析。
put方法
//在队尾插入指定的元素
//如果队列已满,则阻塞线程。
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException(); //插入的元素不能为空,否则抛异常
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly(); //获取“入队锁”
try {
while (count.get() == capacity) { //如果队列已满,则线程在notFull上等待
notFull.await();
}
enqueue(node); //将新的结点连接到“队尾”
c = count.getAndIncrement(); //c表示入队前的队列元素的个数
// 如果现队列长度如果小于容量
// 就再唤醒一个阻塞在notFull条件上的线程
// 这里为啥要唤醒一下呢?
// 因为可能有很多线程阻塞在notFull这个条件上的
// 而取元素时只有取之前队列是满的才会唤醒notFull
// 为什么队列满的才唤醒notFull呢?
// 因为唤醒是需要加putLock的,这是为了减少锁的次数
// 所以,这里索性在放完元素就检测一下,未满就唤醒其它notFull上的线程
// 说白了,这也是锁分离带来的代价
if (c + 1 < capacity) //入队后队列未满,则唤醒一个“入队线程”
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0) //队列从null到非空,则唤醒一个“出队线程”
signalNotEmpty();
}
在插入元素时,首先要获取“入队锁”,如果队列满了,则当前线程需要在notFull条件队列等待;否则。将新元素连接到队列尾部。
从上面的代码分析中可以得出6点结论:
- LinkedBlockingQueue不允许元素为null,这一点在构造方法中也说过了。
- 同一时刻,只能有一个线程执行入队操作,因为putLock在将元素插入到队列尾部时加锁了
- 如果队列满了,那么将会调用notFull的await()方法将该线程加入到Condition等待队列中。await()方法就会释放线程占有的锁,这将导致之前由于被锁阻塞的入队线程将会获取到锁,执行到while循环处,不过可能因为由于队列仍旧是满的,也被加入到条件队列中。
- 一旦一个出队线程取走了一个元素,并通知了入队等待队列中可以释放线程了,那么第一个加入到Condition队列中的将会被释放,那么该线程将会重新获得put锁,继而执行enqueue()方法,将节点插入到队列的尾部
- 然后得到插入一个节点之前的元素个数,如果队列中还有空间可以插入,那么就通知notFull条件的等待队列中的线程。
- 通知出队线程队列为空了,因为插入一个元素之前的个数为0,而插入一个之后队列中的元素就从无变成了有,就可以通知因队列为空而阻塞的出队线程了。
signalNotEmpty()方法只会在put/take之类的入队方法中才会被调用,并且是当队列元素从无到有的时候。下面是signalNotEmpty()方法的实现:
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
//获取takeLock
takeLock.lock();
try {
//释放notEmpty条件队列中的第一个等待线程
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
offer方法
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false; //队列满了之后,会直接返回false
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
offer方法与put方法的区别就是,offer方法将元素插入队列中,如果成功,则返回true,如果队列已经满了,则返回false。 offer还可以设置等待时间,offer(E o, long timeout, TimeUnit unit)方法,如果在等待时间内还不能将元素加入到队列中,则返回失败。
add方法
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
由add方法的源码可以看出,add将元素插入队列中,如果成功返回true;如果当前没有可用空间,则抛IllegalStateException异常。
take方法
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock; //获取出队锁
takeLock.lockInterruptibly();
try {
while (count.get() == 0) { //队列为空,则阻塞线程
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement(); //c表示出队前的元素个数
if (c > 1) //出队前队列非空,则唤醒一个出队线程
notEmpty.signal();
} finally {
takeLock.unlock();
}
//如果队列中的元素从满到非满,通知put线程
if (c == capacity)
signalNotFull();
return x;
}
take方法的源码与put方法是一样的思想,首先获取“出队锁”,然后取走阻塞队列中排在首位的对象,若阻塞队列为null,进入等待状态,直到有新的数据加入队列。
poll方法
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
poll方法与offer方法思想一样,当队列中没有元素时,take方法是阻塞,而poll方法直接返回null.poll方法提供了poll(long timeout, TimeUnit unit)方法,设置限时等待,如果在等待时间内有数据可取,则返回去除的数据,如果超时还没数据可取,则返回null。
LinkedBlockingQueue与ArrayBlockingQueue、LinkedBlockingDeque相比
底层数据存储 | 是否有界 | 锁 | 能否指定公平策略 | |
---|---|---|---|---|
ArrayBlockingQueue | 数组 | 必须指定大小,有界 | 全局锁 | 可以指定公平/非公平策略 |
LinkedBlockingQueue | 单向链表 | 默认为Integer.MAX_VALUE,近似于无界;也可以指定大小 | 锁分离 入队、出队使用不同的锁 | 不能指定公平/非公平策略 |
LinkedBlockingDeque | 双向链表 | 默认为Integer.MAX_VALUE,近似于无界;也可以指定大小 | 全局锁 | 不能指定公平/非公平策略 |
CopyOnWriteArrayList
CopyOnWriteArrayList是JDK1.5,随着J.U.C引入的一个新的集合。它是为了“读多写少”的情景而生的。
CopyOnWriteArrayList运用了一种 “写时复制”的思想。通俗的理解就是当我们需要修改(增/删/改)列表中的元素时,不直接进行修改,而是先将列表Copy,然后在新的副本上进行修改,修改完成之后,再将引用从原列表指向新列表。这样做的好处是读/写是不会冲突的,可以并发进行,操作还是在原列表,写操作在新列表。仅仅当有多个线程同时进行写操作时,才会进行同步。
CopyOnWriteArrayList原理
CopyOnWriteArrayList构造函数
//空参构造
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
final void setArray(Object[] a) {
array = a;
}
//根据已有集合创建
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
//根据已有的数组创建
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
get方法
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
final Object[] getArray() {
return array;
}
根据get方法的源码可知,get方法返回的是内部数组对应索引位置的值。
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); //内部array引用指向新的数组
return true;
} finally {
lock.unlock();
}
}
由于写的操作都是在新数组上进行,不会对并发时的读操作产生影响。
迭代器
CopyOnWriteArrayList对元素进行迭代时,仅仅返回一个当前内部数组的快照,也就是说,如果此时有其它线程正在修改元素,并不会在迭代中反映出来,因为修改都是在新数组中进行的。
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/**
* Snapshot of the array
*/
private final Object[] snapshot;
/**
* Index of element to be returned by subsequent call to next.
*/
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public E next() {
if (!hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
// ...
}
可以看到,上述iterator方法返回一个迭代器对象——COWIterator,COWIterator的迭代是在旧数组上进行的,当创建迭代器的那一刻就确定了,所以迭代过程中不会抛出并发修改异常——ConcurrentModificationException。
另外,迭代器对象也不支持修改方法,全部会抛出UnsupportedOperationException异常。
CopyOnWriteArrayList特点
1、因为CopyOnWriteArrayList使用了“写时复制”,在进行写操作的时候,内存中是存在两个数组的,如果数组内存占用的太大,那么困难会造成频繁的GC,所以CopyOnWriteArrayList并不适合大数据量的场景。
2、CopyOnWriteArrayList只能保证数据的最终一致性,不能保证数据的实时一致性——读操作读到的数据只是一份快照。所以如果希望写入的数据可以立刻被读到,那CopyOnWriteArrayList并不适合。
ConcurrentHashMap
ConcurrentHashMap简介
之前我们都知道hashmap是线程不安全的,想要了解hashmap的移步:集合——HashMap的一些理解与总结
本篇文章将要介绍的 ConcurrentHashMap 是 HashMap 的并发版本,它是线程安全的,并且在高并发的情境下,性能优于 HashMap 很多。本文基于JDK1.8对ConcurrentHashMap进行学习。
ConcurrentHashMap历史演变
jdk 1.7 采用分段锁技术,整个 Hash 表被分成多个段,每个段中会对应一个 Segment 段锁,段与段之间可以并发访问,但是多线程想要操作同一个段是需要获取锁的。所有的 put,get,remove 等方法都是根据键的 hash 值对应到相应的段中,然后尝试获取锁进行访问。JDK1.7ConcurrentHashMap结构如下图所示。
jdk 1.8 取消了基于 Segment 的分段锁思想,改用 CAS + synchronized 控制并发操作,在某些方面提升了性能。并且追随 1.8 版本的 HashMap 底层实现,使用数组+链表+红黑树进行数据存储。
JDK1.8的ConcurrentHashMap结构
ConcurrentHashMap内部维护了一个Node类型的数组,也就是table,数组的每一个位置table[i]代表一个桶,会根据键的hash值映射到不同的桶中,table一共包含四种不同类型的桶:Node、TreeBin、ForwardingNode、ReservationNode。由下图可知,ConcurrentHashMap使用数组+链表+红黑树进行数据存储。TreeBin结点所链接的是一棵红黑树,红黑树的结点用TreeNode表示,所以ConcurrentHashMap实际上一共有5中不同类型的Node结点。
为什么要用TreeBin链接红黑树的头结点而不是直接用TreeNode?
因为红黑树的操作比较复杂,包括构建、左旋、右旋、删除,平衡等操作,用一个代理结点TreeBin来包含这些复杂操作,其实是一种“职责分离”的思想,另外TreeBin中也包含了一些加/解锁的操作。
ConcurrentHashMap构造函数
public ConcurrentHashMap() {
}
/**
* 指定table初始容量的构造器.
* tableSizeFor会返回大于入参(initialCapacity + (initialCapacity >>> 1) + 1)的最小2次幂值
*/
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
有上述ConcurrentHashMap的空参构造可知,当我们new ConcurrentHashMap对象的时候,其实什么操作也没做,带参构造也只是计算了一下table初始容量的大小,并没有创建table数组。这是一种“懒加载”的模式,只有到首次插入键值对的时候,才会真正的初始化table数组。至于为什么将容量设置为2的N次幂,之前在HashMap也有提到过,1、让key均匀分布,减少hash冲突。2、当table数组的大小为2的幂次时,通过key.hash & table.length-1这种方式计算出的索引i,当table扩容后(2倍),新的索引要么在原来的位置i,要么是i+n。
ConcurrentHashMap原理
put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
//对传入的参数进行合法性判断
if (key == null || value == null) throw new NullPointerException();
//计算键所对应的 hash 值
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(); // 初始化table懒加载
//根据键的 hash 值找到哈希数组相应的索引位置
//如果为空,那么以CAS无锁式向该位置添加一个节点
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) //检测到桶结点是 ForwardingNode 类型,协助扩容
tab = helpTransfer(tab, f);
else { //桶结点是普通的结点,锁住该桶头结点并试图在该链表的尾部添加一个节点
V oldVal = null;
synchronized (f) { // 锁住table[i]结点
if (tabAt(tab, i) == f) { // 再判断一下table[i]是不是第一个结点, 防止其它线程的写修改
//向普通的链表中添加元素,无需赘述
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;
}
}
}
//向红黑树中添加元素,TreeBin 结点的hash值为TREEBIN(-2)
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;
}
}
}
}
//binCount != 0 说明向链表或者红黑树中添加或修改一个节点成功
//binCount == 0 说明 put 操作将一个新节点添加成为某个桶的首节点
if (binCount != 0) {
//链表深度超过 8 转换为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); // 链表 -> 红黑树 转换
//oldVal != null 说明此次操作是修改操作
//直接返回旧值即可,无需做下面的扩容边界检查
if (oldVal != null)
return oldVal;
break;
}
}
}
//CAS 式更新baseCount,并判断是否需要扩容
addCount(1L, binCount);
//程序走到这一步说明此次 put 操作是一个添加操作,否则早就 return 返回了
return null;
}
初始化table操作源码如下:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
//如果表为空才进行初始化操作
while ((tab = table) == null || tab.length == 0) {
//sizeCtl 小于零说明已经有线程正在进行初始化操作
//当前线程应该放弃 CPU 的使用
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//否则说明还未有线程对表进行初始化,那么本线程就来做这个工作
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//保险起见,再次判断下表是否为空
try {
if ((tab = table) == null || tab.length == 0) {
//sc 大于零说明容量已经初始化了,否则使用默认容量
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
//根据容量构建数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//计算阈值,等效于 n*0.75
sc = n - (n >>> 2);
}
} finally {
//设置阈值
sizeCtl = sc;
}
break;
}
}
return tab;
}
put操作流程图如下图所示:
ConcurrentHashMap的put()方法执行流程如下:
1、key或value是否为空,如果是则抛异常NullPointerException
2、判断table是否为空或length==0,如果是,则initTable方法初始化table
3、根据键值key计算hash得到插入数组的索引i,判断table[i]是否为null,如果是则直接向table[i]中添加节点,执行第7步;如果不是,判断table[i]的hash值是否为MOVED(即判断该结点是否为ForwardingNode 结点),如果发现该结点为 ForwardingNode 结点,表明当前的哈希表正在扩容和 rehash,则帮忙扩容。
4、判断table[i]是否为普通链表结点,如果是,则判断key是否存在,如果key存在,则更新value;key不存在,则直接插入到链表尾部
5、判断链表长度是否大于8,如果是,判断table的大小是否大于64,如果table大小大于64,则将链表转换为红黑树;否则,将table扩容
6、如果table[i]不是链表结点,则判断是否为treeBin结点,如果是,则红黑树插入。
7、addCount计数加1并判断是否需要扩容。
get方法
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值
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) { // table[i]就是待查找的项,直接返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0) // hash值<0, 说明遇到特殊结点(非链表结点), 调用find方法查找
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) { // 按链表方式查找
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
get方法的逻辑很简单,首先根据key的hash值计算映射到table的哪个桶——table[i]。
1、如果table[i]的key和待查找key相同,那直接返回;
2、如果table[i]对应的结点是特殊结点(hash值小于0),则通过find方法查找;
3、如果table[i]对应的结点是普通链表结点,则按链表方式查找。
ConcurrentHashMap的扩容
扩容的时机
前面已经说了,这里不再赘述。见流程图。
扩容方法
/**
* 尝试对table数组进行扩容.
*
* @param 待扩容的大小
*/
private final void tryPresize(int size) {
// 视情况将size调整为2的幂次
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;
// table还未初始化,则先进行初始化
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;
}
}
}
//c <= sc说明已经被扩容过了;n >= MAXIMUM_CAPACITY说明table数组已达到最大容量
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
//进行table扩容
else if (tab == table) {
int rs = resizeStamp(n); //根据容量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;
// 协助数据转移, 把正在执行transfer任务的线程数加1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// sc置为负数, 当前线程自身成为第一个执行transfer(数据转移)的线程
// 这个CAS操作可以保证,仅有一个线程会执行扩容
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
扩容分为以下两种情况
1、已经有其它线程正在执行扩容了,则当前线程会尝试协助“数据迁移”;(多线程并发)
2、没有其它线程正在执行扩容,则当前线程自身发起扩容。(单线程)
注意:这两种情况都是调用了transfer方法,通过第二个入参nextTab进行区分(nextTab表示扩容后的新table数组,如果为null,表示首次发起扩容)。
第二种情况下,是通过CAS和移位运算来保证仅有一个线程能发起扩容。