文章目录
二、集合
1.BlockingQueue
1.1 简介
BlockingQueue是一个接口,用于实现带阻塞功能的队列,类似于消费者生产者阻塞队列,队列中按照先进先出(FIFO)的原则对元素进行排序。
1.2 注意事项
BlockingQueue有如下注意事项:
- BlockingQueue不接受空元素
实现在尝试添加、放置或提供null时抛出NullPointerException - BlockingQueue可能是容量有限的
允许设置容量,在未超出容量时,可以无阻塞的防止元素,不设置容量时,容量最大值为Integer.MAX_VALUE(无界队列) - BlockingQueue继承自Collection接口,使用Collection提供的批量操作的方法非线程安全
例如addAll、containsAll、retainAll和removeAll不一定以原子方式执行
1.3 方法介绍
1.3.1 插入
/**
* 插入队列
* 适用:有界队列
* @param e 队列元素
* @return 插入是否成功
* @throws IllegalStateException 没有可用空间
*/
boolean add(E e);
/**
* 插入队列
* 适用:无界队列
* @param e 队列元素
* @return 插入是否成功
*/
boolean offer(E e);
/**
* 插入队列,空间满时等待,一直阻塞到超时为止
* @param e 队列元素
* @param timeout 阻塞超时时间
* @param unit 等待时间单位
* @return 插入是否成功
* @throws InterruptedException 中断异常
*/
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
/**
* 插入队列,空间满时等待,一直阻塞到有空间为止
* @param e 队列元素
* @throws InterruptedException 中断异常
*/
void put(E e) throws InterruptedException;
1.3.2 删除
/**
* 获取并删除队列头元素,队列中没有元素时等待,一直阻塞到有元素为止
* @return 队列头元素
* @throws InterruptedException 中断异常
*/
E take() throws InterruptedException;
/**
* 取并删除队列头元素,队列中没有元素时,返回null
* @return
*/
E poll();
/**
* 获取并删除队列头元素,队列中没有元素时等待,一直阻塞到超时为止
* @param timeout 阻塞超时时间
* @param unit 阻塞超时时间单位
* @return
* @throws InterruptedException 中断异常
*/
E poll(long timeout, TimeUnit unit) throws InterruptedException;
/**
* 如果存在该元素,删除,存在多个,只删除一个
* @param o 被删除的元素
* @return 是否删除成功
*/
boolean remove(Object o);
1.3.3 查看
/**
* 检索但不删除此队列的头部。此方法与peek的唯一区别在于,如果队列为空,则抛出异常。
* @throws NoSuchElementException – 队列空
* @return 队列的头部
*/
E element();
/**
* 检索但不删除该队列的头部,如果该队列为空则返回null
* @return 队列的头部或Null
*/
E peek();
1.3.4 其他
/**
* 队列中是否包含某元素
* @param o 查询的元素
* @return 队列中是否包含某元素
*/
public boolean contains(Object o);
/**
* 获取剩余容量
* @return 剩余容量
*/
int remainingCapacity();
/**
* 从该队列中删除所有可用的元素,并将它们添加到给定的集合中,此操作可能比重复轮询该队列更有效
* @param c 待赋值的集合
* @throws UnsupportedOperationException 如果指定的集合不支持添加元素
* @throws IllegalArgumentException 如果指定的集合是这个队列,或者这个队列的某个元素的属性阻止它被添加到指定的集合
* @return
*/
int drainTo(Collection<? super E> c);
/**
* 最多从该队列中删除给定数量的可用元素,并将它们添加到给定的集合中
* @param c 待赋值的集合
* @param maxElements 最多删除并赋值的数量
* @throws UnsupportedOperationException 如果指定的集合不支持添加元素
* @throws IllegalArgumentException 如果指定的集合是这个队列,或者这个队列的某个元素的属性阻止它被添加到指定的集合
* @return
*/
int drainTo(Collection<? super E> c, int maxElements);
1.3.5 对比
BlockingQueue的方法可以划分为如下四种类型,对应类型的方法如下表所示
- 抛异常:
如果试图的操作无法立即执行,抛一个异常。 - 特定值:
如果试图的操作无法立即执行,返回一个特定的值(常常是true/false)。 - 阻塞:
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。 - 超时
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是true / false)。
抛异常 | 特定值 | 阻塞 | 超时 | |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
删除 | remove(o) | poll() | take() | poll(time, unit) |
查询 | element() | peek() |
1.4 子类介绍
1.4.1 继承类图
如下图所示,BlockingQueue的子类如下
1.4.2 ArrayBlockingQueue
ArrayBlockingQueue是基于数组的有界队列,在初始化时,必须指定容量,支持公平锁和非公平锁,其使用方式如下:
//可支持公平或非公平锁
BlockingQueue<String> fair = new ArrayBlockingQueue<>(50,false);
BlockingQueue<String> queue = new ArrayBlockingQueue<>(50);
queue.put("1");
System.out.println(queue.take());
1.4.3 LinkedBlockingQueue
LinkedBlockingQueue是一种基于单向链表的有界队列。因为队头和队尾是2个指针分开操作的,所以用了2把锁+2个条件,同时有1个AtomicInteger的原子变量记录count数,意味着put和put互斥,take和take互斥,put和take不互斥,其使用方式如下:
//不指定容量,容量为Integer.MAX_VALUE
BlockingQueue<String> unbounded = new LinkedBlockingQueue<String>();
//指定容量
BlockingQueue<String> bounded = new LinkedBlockingQueue<String>(50);
bounded.put("1");
System.out.println(bounded.take());
1.4.4 PriorityBlockingQueue
PriorityBlockingQueue是一个按照优先级从小到大出列的无界队列,插入元素必须实现 java.lang.Comparable 接口,优先级相同的元素无法保证顺序,Iterator遍历不是按照优先级排序
BlockingQueue<String> queue = new PriorityBlockingQueue<>(5);
//String本身就实现了Comparable接口
queue.put("2");
queue.put("1");
System.out.println(queue.take());
System.out.println(queue.take());
1.4.5 DelayQueue
DelayQueue是一个按延迟时间从小到大出列的无界队列,入队的元素必须实现Delayed接口, 除队列满或空外,未到延时期也会造成阻塞,可用于定时任务调度的实现
public static class MyDelayData implements Delayed {
/**
* 数据
*/
private final String data;
/**
* 剩余生存时间
*/
private final long availableTime;
public String getData() {
return data;
}
public long getAvailableTime() {
return availableTime;
}
/**
* 初始化
* @param data 数据
* @param delayTime 延迟时间
*/
public MyDelayData(String data, long delayTime) {
this.data = data;
this.availableTime = delayTime + System.currentTimeMillis();
}
/**
* 还剩多少时间到期 小于0到期
* @param unit 时间单位枚举
* @return
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(availableTime - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
}
/**
* 排序
* @param o
* @return
*/
@Override
public int compareTo(Delayed o) {
MyDelayData others = (MyDelayData) o;
return (int)(this.availableTime - others.getAvailableTime());
}
}
public static void main(String[] args) throws InterruptedException {
BlockingQueue<MyDelayData> queue = new DelayQueue<>();
queue.put(new MyDelayData("剩余时间为50ms的数据",50));
queue.put(new MyDelayData("剩余时间为100ms的数据",100));
System.out.println(queue.take().getData());
System.out.println(queue.take().getData());
}
1.4.6 SynchronousQueue
SynchronousQueue是一个不存储元素的无界阻塞队列,生产和消费必须一一对应才不会阻塞,如先线程1调用put后不存在任意一个线程调用take,故阻塞,线程2调用take后,线程1才被唤醒,也可以多个线程同时调用put或take,所有调用的线程都被阻塞,直到有其他线程调用take或put匹配后才唤醒
public static void main(String[] args) throws InterruptedException {
//可传入公平或非公平
BlockingQueue<String> fair = new SynchronousQueue<>(true);
BlockingQueue<String> queue = new SynchronousQueue<>();
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程"+Thread.currentThread()+"放入元素前");
queue.put("1");
System.out.println("线程"+Thread.currentThread()+"放入元素后");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
System.out.println("主线程获取"+queue.take());
}
1.4.7 LinkedTransferQueue
LinkedTransferQueue是一个链表组成的无界阻塞队列,也需要消费与生产一一匹配,可以理解为 SynchronousQueue +LinkedBlockingQueue ,性能比 LinkedBlockingQueue 更高(没有锁操作),比 SynchronousQueue能存储更多的元素。比其他队列多出在TransferQueue中定义的方法,如tryTransfer和transfer。
public static void main(String[] args) throws InterruptedException {
TransferQueue<String> queue = new LinkedTransferQueue<>();
new Thread(new Runnable() {
@Override
public void run() {
try {
String data = "元素1";
//是否有消费者接收,如果有立即放入元素,没有什么都不做
if (queue.tryTransfer(data)) {
System.out.println("已放入元素");
}else {
System.out.println("尝试阻塞放入前");
//阻塞式放入,没有消费者就阻塞
queue.transfer(data);
System.out.println("尝试阻塞放入后");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
System.out.println("消费者开始消费"+queue.take());
}
1.4.8 BlockingDeque
BlockingDeque继承自BlockingQueue,是一个带阻塞的双端队列,即头部和尾部都可以进行元素添加或删除操作,通常在线程即是消费者又是生产者的情况下使用(工作窃取队列),拥有BlockingQueue的功能的同时,又扩展了一部分接口,xxxFirst表示从队列头进行操作,xxxLast表示从队列尾进行操作,如下表所示
抛异常 | 特定值 | 阻塞 | 超时 | |
---|---|---|---|---|
插入 | addFirst(e) | offerFirst(e) | putFirst(e) | offerFirst(e, time, unit) |
删除 | removeFirst(o) | pollFirst() | takeFirst() | pollFirst(time, unit) |
查询 | getFirst() | peekFirst() |
抛异常 | 特定值 | 阻塞 | 超时 | |
---|---|---|---|---|
插入 | addLast(e) | offerLast(e) | putLast(e) | offerLast(e, time, unit) |
删除 | removeLast(o) | pollLast() | takeLast() | pollLast(time, unit) |
查询 | getLast() | peekLast() |
1.4.9 LinkedBlockingDeque
LinkedBlockingDeque是由双向列表组成的阻塞队列,队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。
BlockingDeque<String> queue = new LinkedBlockingDeque<>();
queue.putFirst("1");
queue.putLast("2");
System.out.println(queue.takeFirst());
System.out.println(queue.takeLast());
1.4.10 对比
BlockingQueue子类对比如下表,在使用无界队列时,需要避免OOM的隐患
子类 | 初始化容量 | 公平性 | 数据结构 | 有界性 |
---|---|---|---|---|
ArrayBlockingQueue | 构造函数设置 | 构造函数设置 | 数组 | 有界 |
LinkedBlockingQueue | 构造函数设置,未设置时为Integer.MAX_VALUE | 非公平 | 单链表 | 有界 |
PriorityBlockingQueue | 构造函数设置,未设置时为11 | 非公平 | 数组实现的二叉小根堆 | 无界(扩容) |
DelayQueue | 11 | 非公平 | PriorityQueue(数组实现的二叉小根堆) | 无界(扩容) |
SynchronousQueue | 无容量,不存储元素 | 构造函数设置 | 公平TransferQueue,非公平TransferStack,底层都为单链表 | 无界 |
LinkedTransferQueue | 无初始容量 | 公平 | 单链表 | 无界 |
LinkedBlockingDeque | 构造函数设置,未设置时为Integer.MAX_VALUE | 非公平 | 双向链表 | 有界 |
2.CopyOnWriteArrayList/Set
2.1 简介
CopyOnWriteArrayList是基于数组实现CopyOnWrite(写时复制,即读完全无锁,写时拷贝新数组进行写入,写入完成后再替换原数组的技术),同样实现该技术的还有CopyOnWriteArraySet,其内部使用CopyOnWriteArrayList并在此基础上封装了去重的操作,故本节重点讨论CopyOnWriteArrayList。
2.2 fail-fast
2.2.1 定义
fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。
当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出 ConcurrentModificationException异常。
2.2.2 出现场景
2.2.2.1 单线程
在常见的集合如ArrayList,HashMap等遍历的过程中,出现新增或删除等涉及到集合元素变化时就会触发fail-fast,但并不保证一定触发fail-fast,如下面示例的先增加再删除
//添加元素抛ConcurrentModificationException异常
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
arrayList.add("1");
for (String s : arrayList) {
//添加元素
arrayList.add("1");
}
}
//删除元素抛ConcurrentModificationException异常
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
arrayList.add("1");
for (String s : arrayList) {
//删除元素
arrayList.remove(s);
}
}
//修改元素不抛异常
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
arrayList.add("1");
for (String s : arrayList) {
//修改元素
arrayList.set(0,"update");
}
}
//先增加再删除 不抛异常
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
arrayList.add("1");
for (String s : arrayList) {
//添加元素
arrayList.add("1");
//删除元素
arrayList.remove(s);
}
}
2.2.2.2 多线程
在多线程的环境下,一个线程使用迭代器进行遍历,另一个线程同时进行增加或删除也会出现fail-fast
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
map.put(i + "", i + "");
}
//一个线程读
new Thread(new Runnable() {
@Override
public void run() {
for (String key : map.keySet()) {
System.out.println(map.get(key));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
//一个线程删
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.remove(i+"");
}
}
}).start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(map);
}
运行结果如下
0
1
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445)
at java.util.HashMap$KeyIterator.next(HashMap.java:1469)
{}
2.2.3 原理分析
首先定位异常抛出的地方at java.util.HashMap$HashIterator.nextNode(HashMap.java:1445)
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
可以看出modCount != expectedModCount是异常出现的根本原因,expectedModCount是构造器初始化时就定义好不会改变的值,modCount 随着集合的元素的变化,如新增或删除或清除集合等,会发生改变,故对元素结构变化时会引发ConcurrentModificationException异常
2.2.3 避免fail-fast
可以采用如下方式避免
-
使用Iterator提供的操作方法
如add,remove方法不会引起modCount的变化 -
使用并发容器
如CopyOnWriterArrayList、ConcurrentHashMap
2.3 实现原理
2.2.1 写入
add 处理流程为,先复制一个数组,长度为原长度+1,然后把新值存放,最后赋值给原地址,set与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);
// 存放元素e
newElements[len] = e;
// 赋值给原地址
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
remove处理流程为,计算删除的元素所在索引,分两段复制到同一个数组,索引前的和索引后的,不复制索引所在位置
public E remove(int index) {
// 可重入锁
final ReentrantLock lock = this.lock;
// 获取锁
lock.lock();
try {
// 获取数组
Object[] elements = getArray();
// 数组长度
int len = elements.length;
// 获取旧值
E oldValue = get(elements, index);
// 需要移动的元素个数
int numMoved = len - index - 1;
if (numMoved == 0) // 移动个数为0
// 复制后设置数组
setArray(Arrays.copyOf(elements, len - 1));
else { // 移动个数不为0
// 新生数组
Object[] newElements = new Object[len - 1];
// 复制index索引之前的元素
System.arraycopy(elements, 0, newElements, 0, index);
// 复制index索引之后的元素
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
// 设置索引
setArray(newElements);
}
// 返回旧值
return oldValue;
} finally {
// 释放锁
lock.unlock();
}
}
2.2.2 读取
以get为例,所有读操作未进行加锁
private E get(Object[] a, int index) {
return (E) a[index];
}
public E get(int index) {
return get(getArray(), index);
}
2.2.3 迭代
内部使用COWIterator迭代器,该迭代器的特点是持有元素集合的快照,不会抛出ConcurrentModificationException异常,但查询到的快照数据不具有强一致性,可能与元素组数据不一致,同时也不支持迭代器内部提供的remove、set 和 add方法
static final class COWIterator<E> implements ListIterator<E> {
// 快照
private final Object[] snapshot;
// 游标
private int cursor;
// 构造函数
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
// 不支持remove操作
public void remove() {
throw new UnsupportedOperationException();
}
// 不支持set操作
public void set(E e) {
throw new UnsupportedOperationException();
}
// 不支持add操作
public void add(E e) {
throw new UnsupportedOperationException();
}
}
2.4 缺陷
CopyOnWriteArrayList有如下缺陷:
-
内存消耗大
每一次写入都会复制数组,导致内存消耗大,可能导致young gc或者full gc,只有在读多写少的情况下才适用 -
数据不实时
复制数组并写入会消耗一定时间,遍历或读取时获取到的不一定是最新的数据
3.ConcurrentLinkedQueue/Deque
3.1 简介
ConcurrentLinkedQueue是基于CAS实现的单链表队列,ConcurrentLinkedDeque是基于CAS实现的双链表队列。由于使用了无锁,所以性能优于阻塞队列的实现,在并发量较大的情况可以考虑使用该队列,但存取数据的逻辑需要自行实现。
3.2 HOPS(延迟更新策略)
HOPS是一种延迟更新策略,即对tail指针(入队)和head指针(出队)的CAS操作,每两个节点更新一次,在高并发的常见下,会提升一定的效率。
3.2.1 tail更新时机
tail指向的下一个节点不为NULL时,cas更新tail指针,间隔一次更新
3.2.2 head更新时机
head指向的下一个节点为NULL时,cas更新head指针,间隔一次更新,在获取队列的大小时,需要从第一个非空的节点开始计数,因为head指针的更新不及时
4.ConcurrentHashMap
4.1 JDK1.7
4.1.1 结构图
如下图所示ConcurrentHashMap,由16个Segment组成的数组组成,一个Segment存放HashEntry数组,HashEntry数组中存放由HashEntry组成的链表,基于分段锁机制控制并发。
4.1.2 初始化
ConcurrentHashMap初始化要点如下
-
Segment数组初始化为16,不可扩容
ConcurrentHashMap采用分段锁实现线程安全,在进行操作时,并非对整个ConcurrentHashMap加锁,而是对16个Segment加锁,故并发度为16且不可变更,在初始化时会初始化Segment[0],其余Segment初始化在put时进行初始化,为避免并发使用cas进行设置。 -
initialCapacity、loadFactor、concurrencyLevel
initialCapacity 初始化容量 默认值16
loadFactor 负载因子 默认值0.75
concurrencyLevel 并发度 默认值16
这三个参数是计算HashEntry数组初始化长度及扩容的核心参数,可在构造方法中设置 -
HashEntry数组长度cap计算
初始化数组长度cap,最小值为2(MIN_SEGMENT_TABLE_CAPACITY = 2),若计算出来初始化长度小于2,则修改为2,避免插入第一个元素就扩容
计算公式为:initialCapacity / concurrencyLevel(向上取整) -
HashEntry数组扩容阈值(threshold)计算
扩容阈值计算公式为:cap * loadFactor ,如2*0.75=1.5,HashEntry数组大于1.5时进行扩容,及等于2时扩容。
在设置initialCapacity 时可以采用如下方式进行估算:
initialCapacity = cap(期望设置的容量) / loadFactor + 1
4.1.3 扩容
ConcurrentHashMap扩容流程为,当数组长度超过扩容阈值时,新建长度为原数组两倍的数组,重新计算hash值对应的槽,设置完成后将新数组地址赋给原地址,该流程在JDK1.8节详细分析。
4.2 JDK1.8
4.2.1 结构图
1.8更改了ConcurrentHashMap的结构,与HashMap类似,扩展了红黑树和链表的相互转化,锁加在Node数组中的每一个节点上,Node数组可以进行扩容,并发度取决于Node数组的长度。
4.2.2 初始化
ConcurrentHashMap初始化要点如下
-
ConcurrentHashMap(int initialCapacity)容量cap计算方式
cap = 向上取最近的 2 的 n 次方(1.5 * initialCapacity +1),如initialCapacity 为64,cap等于128 -
ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel)容量cap计算方式
cap = 向上取最近的 2 的 n 次方((long)(1.0 + (long)initialCapacity / loadFactor))
4.2.3 put过程
4.2.3.1 Node数组初始化
使用CAS设置sizeCtl变量为-1,设置成功的线程进行初始化工作,其他线程自旋等待直到初始化成功停止
4.2.3.2 Node[i]链表初始化
初始化Node结点并放入Node数组中
4.2.3.3 扩容
其他线程正在扩容,帮助其完成扩容
4.2.3.4 元素put
使用synchronized对Node加锁,加锁后根据头结点判断属于红黑树还是链表(TreeBin继承Node),执行元素put操作。
操作完成后累加当前链表或红黑树的元素个数(binCount),判断是否需要转为红黑树转化。
红黑树与链表相互转化的条件,如下所示:
- binCount >= TREEIFY_THRESHOLD,tab.length < MIN_TREEIFY_CAPACITY转为红黑树
TREEIFY_THRESHOLD (树转化阈值)= 8 ,MIN_TREEIFY_CAPACITY(最小转化数组容量) = 64 - binCount <= UNTREEIFY_THRESHOLD
UNTREEIFY_THRESHOLD(树退回链表阈值)= 6
4.2.4 扩容
4.2.4.1 sizeCtl
sizeCtl可以理解为HashMap的状态变量,通过CAS进行设置,有如下几种情况
- sizeCtl = cap,tab=null
未初始之前的初始容量 - sizeCtl = -1
表示整个HashMap正在初始化 - sizeCtl < -1
表示整个HashMap正在扩容,在按步长进行分段扩容时,sizeCtl 会被设置为较大的负数,每一个线程扩容完毕,sizeCtl +1,全部扩容完成后设置为下次扩容的阈值 - sizeCtl < 0
表示整个HashMap正在初始化 - sizeCtl = 下次扩容的阈值(数组长度 * 0.75)> 0
表示扩容完成
4.2.4.2 扩容流程
扩容时,将整个tab的从后到前,划分成tab.len (Node数组长度)/ stride(步长)个段,每一段可由不同的线程并发执行,用transferIndex(初始时为Node数组长度)记录当前执行的进度,每一个线程执行完毕后CAS操作transferIndex减去固定的步长,当transferIndex为0时扩容结束
- stride值计算
单核 = Node数组长度
多核 =(Node数组长度>>>3)/CPU核数 - 扩容时转发节点
已扩容完成的Node在旧的hashMap中会变为ForwardingNode(转发节点),该节点记录新hashMap的值引用,避免获取到旧的引用 - 数据迁移
节点rehash满足如下规律:
节点hashCode & 原数组长度 n = 0 ,节点为低位,新节点所在数组下标i = 原数组下标i
节点hashCode & 原数组长度 n = 1 ,节点为高位,新节点所在数组下标i = 原数组下标i + 原数组长度n
第一步:计算lastRun节点(从这个节点开始后面的所有节点全为低位或者全为高位的节点称为lastRun节点),将其之后的所有链表放到高位或低位链表中。
第二步:从节点头开始到lastRun节点结束,依次将节点放入高位或低位链表中
第三步:直接将高位链表放在新数组i+n位置,低位链表放在新数组i位置
图片转自:https://blog.csdn.net/ZOKEKAI/article/details/90051567
5.ConcurrentSkipListMap/Set
5.1 简介
ConcurrentHashMap是key无序的线程安全的map,ConcurrentSkipListMap是key有序的线程安全的map,同样的TreeMap是基于红黑树实现的key有序的非线程安全的map,目前尚未找到如何实现高效无锁的树,故使用基于链表的跳跃表来实现。
ConcurrentSkipListSet是对ConcurrentHashMap的封装,本节只介绍ConcurrentSkipListMap
5.2 结构图
由于链表本身具有插入删除快但搜索慢的特点,为了提高搜索效率,为有序链表建立索引可以有效减低链表搜索的时间复杂度,通过判断元素是否在某个区间,逐层查找,直到找到目标元素为止,是一种空间换时间的算法。
5.2 实现原理
在并发环境下,由于key无序从头部或尾部进行插入可以有效避免并发问题,但在有序链表中不可避免的需要在中间插入或删除元素,若存在两个线程同时进行插入或修改,就有可能导致插入到已经被删除元素的节点后面,为了避免这个问题,ConcurrentSkipListMap采用使用将value值赋null的方式标记已删除的元素,在执行查找、删除、添加时,都会对node的value值进行校验,若为空,执行真正的删除操作解决并发问题。