我们知道HashMap是一种键值对形式的数据存储容器,它内部的元素是无序的。当然了,JDK也提供了TreeMap,TreeMap使用红黑树按照key的顺序(自然顺序、自定义顺序)来使得键值对有序存储,但是和HashMap同样是线程不安全的,因此在JAVA并发包中提供了ConcurrentSkipListMap容器,它能够保证在多线程环境下使键值对按照key的顺序来存储。
ConcurrentSkipListMap的底层是通过跳表来实现的,跳表(SkipList) 是一种随机化的数据结构, 通过“空间来换取时间”的一个算法,建立多级索引来进行查找, 时间复杂度和红黑树一样为O(log n),实现却比红黑树简单的多。为了更清晰的了解这种数据结构,我们画图来讲解一下。
跳表的数据结构
首先我们普通的链表如下图所示
我们知道链表这种数据结构查找起来是非常麻烦的,往往需要通过遍历的形式,效率非常之低,如上图的链表,如果我们查询9,21,30这三个元素,那么就需要依次遍历分别需要比较3,6,8次,当元素比较多时,这个过程就会非常耗时,而跳表的解决思路是怎样的呢?跳表会从链表中挑选出一些元素作为比较的索引,如下图
这样呢我们就可以先与上层的索引进行比较,就可以跳过一些不必要的比较来提高效率。
SkipList具备如下特性:
如下,就是一个典型的跳表结构
基于上图,例如我们要查找元素21的话
首先从最高层开始比较,比较3,比3大,然后比较9,比9大,然后比较25,比25小,然后从9的下一层开始查找,比较16,比16大,又比较25,比25小,又从下一层找,直到找到21.
这个过程元素少的时候不明显,如果元素很多,就能看到性能显著的提升。
然后我们来说下插入操作,首先需要查找合适的位置。并且在确认新节点要占据的层次K时,是完全随机的。如果占据的层次K大于链表的层次,则重新申请新的层,否则插入指定层次。
例如我们要插入一个23这个元素,那么如果随机生成的k>3时,则需要申请新的层
如果k=2,则如下,这个过程是完全随机的。
删除操作很简单,找到节点,删除节点,调整指针即可,但是ConcurrentSkipListMap删除操作还有一个很特别的地方,后面我们会从源码分析。
源码分析
首先我们来看下类中的重要属性。
我们先来看下Node节点类 。
Node的结构和一般的单链表节点毫无区别,key-value和一个指向下一个节点的next。是最基本的存储单元
static final class Node<K,V> {
final K key;
volatile Object value;
volatile Node<K,V> next;
Node(K key, Object value, Node<K,V> next) {
this.key = key;
this.value = value;
this.next = next;
}
//省略
.....
}
Index是一个基于Node节点的索引Node,里面一个指向下一个Index的right,一个指向下层的down节点。
static class Index<K,V> {
final Node<K,V> node;
final Index<K,V> down;
volatile Index<K,V> right;
/**
* Creates index node with given values.
*/
Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
this.node = node;
this.down = down;
this.right = right;
}
//省略
}
HeadIndex类封装了Index类,作为每层的头结点,有一个level来定义层级。
static final class HeadIndex<K,V> extends Index<K,V> {
final int level;
HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
super(node, down, right);
this.level = level;
}
}
接下来是一些重要属性
//头节点指向的Node类
private static final Object BASE_HEADER = new Object();
//整个跳表的头结点,通过它可以遍历访问整张跳表。
private transient volatile HeadIndex<K,V> head;
//比较器
final Comparator<? super K> comparator;
接下来我们来看下该类的构造方法
public ConcurrentSkipListMap() {
this.comparator = null;
initialize();
}
public ConcurrentSkipListMap(Comparator<? super K> comparator) {
this.comparator = comparator;
initialize();
}
public ConcurrentSkipListMap(Map<? extends K, ? extends V> m) {
this.comparator = null;
initialize();
putAll(m);
}
public ConcurrentSkipListMap(SortedMap<K, ? extends V> m) {
this.comparator = m.comparator();
initialize();
buildFromSorted(m);
}
我们发现它们都调用了initialize方法来初始化
private void initialize() {
keySet = null;
entrySet = null;
values = null;
descendingMap = null;
//初始化头节点
head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),
null, null, 1);
}
接下来就是重中之重了, 我们来看下插入元素的put方法,过程比较复杂。
public V put(K key, V value) {
//值不能为null
if (value == null)
throw new NullPointerException();
return doPut(key, value, false);
}
private V doPut(K key, V value, boolean onlyIfAbsent) {
Node<K,V> z; // added node
//判断key是否为null
if (key == null)
throw new NullPointerException();
//获取比较器
Comparator<? super K> cmp = comparator;
outer: for (;;) {
//根据 key,找到待插入的位置
//b为前驱节点,将来作为新加入结点的前驱节点
//n为后继结点,将来作为新加入结点的后继结点
//新节点将插入在 b 和 n 之间
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
//如果n为 null,那么说明b是链表的尾节点,这种情况比较简单,直接构建新节点插入即可
//如果不为null则执行下面代码
if (n != null) {
Object v; int c;
//获取n的后继节点
Node<K,V> f = n.next;
//如果n不再是b的后继结点,说明在此期间有其他线程向b后面添加了新元素
//那么我们直接退出循环,重新计算新节点将要插入的位置
if (n != b.next) // inconsistent read
break;
//value =null说明n已经被标识为待删除,其他线程正在进行删除操作
//在后面删除源码时会讲到
//调用helpDelete帮助删除,并退出循环重新计算待插入位置
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
//如果b.value为空,说明前驱节点b已经被删除了,退出循环重新计算插入位置
if (b.value == null || v == n) // b is deleted
break;
//如果新节点的key>n.key,说明找到的前驱节点有误
//按顺序往后挪一个位置,退出当前循环,重新比较
if ((c = cpr(cmp, key, n.key)) > 0) {
b = n;
n = f;
continue;
}
//新节点的 key 等于 n 的 key,CAS 更新即可
if (c == 0) {
if (onlyIfAbsent || n.casValue(v, value)) {
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
break; // restart if lost race to replace value
}
// else c < 0; fall through
}
//构建新的节点
z = new Node<K,V>(key, value, n);
//CAS插入
if (!b.casNext(n, z))
break; // restart if lost race to append to b
break outer;
}
}
//上面做的操作就是将元素插入到最底层的level
//接下来要做的就是生成随机level层,来决定这个元素在哪些层存在
//首先获取一个随机数,四个字节,32位
int rnd = ThreadLocalRandom.nextSecondarySeed();
//和 1000 0000 0000 0000 0000 0000 0000 0001 进行与运算
//如果不等于 0,那么将仅仅把新节点插入到最底层的链表中即可,不会往上层插入
//如果等于 0,说明这个随机数最高位和最低位都为 0,这种概率很大
if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
int level = 1, max;
//用低位连续为 1 的个数作为 level 的值,随机策略
while (((rnd >>>= 1) & 1) != 0)
++level;
Index<K,V> idx = null;
HeadIndex<K,V> h = head;
//如果得到的level 在当前跳表 level 范围内
//构建一个从 1 到 level 的纵列 index 结点链表
if (level <= (max = h.level)) {
for (int i = 1; i <= level; ++i)
idx = new Index<K,V>(z, idx, null);
}
//否则需要新增一个 level 层
else { // try to grow by one level
level = max + 1; // hold in array and later pick the one to use
//创建一个index节点数组
@SuppressWarnings("unchecked")Index<K,V>[] idxs =
(Index<K,V>[])new Index<?,?>[level+1];
//构建一个从 1 到 level 的纵列 index 结点链表
for (int i = 1; i <= level; ++i)
idxs[i] = idx = new Index<K,V>(z, idx, null);
//更新头节点
for (;;) {
h = head;
int oldLevel = h.level;
//level 肯定是比 oldLevel 大一的,如果小了说明其他线程更新过表了
if (level <= oldLevel) // lost race to add level
break;
HeadIndex<K,V> newh = h;
Node<K,V> oldbase = h.node;
//更新头指针
for (int j = oldLevel+1; j <= level; ++j)
newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
if (casHead(h, newh)) {
h = newh;
idx = idxs[level = oldLevel];
break;
}
}
}
// 上面代码做的只是创建level个节点并且纵向关联,但是横向并没有关联,接下来要做的就是横向关联
splice: for (int insertionLevel = level;;) {
int j = h.level;
for (Index<K,V> q = h, r = q.right, t = idx;;) {
//其他线程并发操作导致头结点被删除,直接退出外层循环
if (q == null || t == null)
break splice;
if (r != null) {
Node<K,V> n = r.node;
// compare before deletion check avoids needing recheck
int c = cpr(cmp, key, n.key);
//如果 n 正在被其他线程删除,那么调用 unlink 去删除它
if (n.value == null) {
if (!q.unlink(r))
break;
//重新获取q的右节点,再次进入循环
r = q.right;
continue;
}
//c > 0 说明前驱结点定位有误,重新进入
if (c > 0) {
q = r;
r = r.right;
continue;
}
}
if (j == insertionLevel) {
//尝试着将 t 插在 q 和 r 之间,如果失败了,退出内循环重试
if (!q.link(r, t))
break; // restart
//如果插入完成后,t 结点被删除了,那么结束插入操作
if (t.node.value == null) {
findNode(key);
break splice;
}
// 标志的插入层 -- ,如果== 0 ,表示已经到底了,插入完毕,退出循环
if (--insertionLevel == 0)
break splice;
}
// 上面节点已经插入完毕了,插入下一个节点
if (--j >= insertionLevel && j < level)
t = t.down;
q = q.down;
r = q.right;
}
}
}
return null;
}
我们画图来讲解一下这个过程,首先初始化一个ConcurrentSkipListMap如下图所示:
添加 key=1, value = A ,key=2,value=B节点,寻找前继节点, 这时返回的 b = BaseHeader, n = null,所以直接插入,然后假设获取的 level 是0(要知道获得0的概率是很大的, 这个函数返回的最大值也就31, 也就是说, 最多有31层的索引),idx = null, 直接break 出去,操作结束
接下来再添加key=3,Value=C节点,寻找前继节点, 这时返回的 b = node2, n = null,所以直接插入。这时我们假设我们获取到 level = 1, 则步骤14 中 level <= max(max = 1)成立,初始化一个 idx,最终找到要插入index位置, 进行link操作,此时情况如下:
然后我们来添加key=4,Value=D,这个和上面一样,直接插入在后面即可,然后我们来插入key=5,value=e这个节点,首先寻找前继节点, 这时返回的 b = node2, n = null,所以直接插入,然后假设我们获取到 level = 25, 则 level <= max(max = 1)不成立,所以这时候我们就要多加一层了,首先进行idx 链表的初始化, 一共两个链表节点 (idx是纵向的链表),然后增加一层HeadIndex,
其实doPut方法就是获取key对应的前继节点, 然后cas设置next值, 随后 生成随机 level(0-31之间), 若新的 level <= oldMaxLevel 则增加对应的索引层, 若level > oldMaxLevel, 则 HeadIndex 也会随之增加索引层;
接下来我们来看看该方法中调用的几个重要方法。
首先看一下寻找前驱节点的方法,思路是从矩形链表的左上角的 HeadIndex 索引开始, 先向右, 遇到 null, 或 > key 时向下, 重复向右向下找, 一直找到 对应的前继节点(前继节点就是小于 key 的最大节点)
private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
if (key == null)
throw new NullPointerException(); // don't postpone errors
for (;;) {
//q是最顶层HeadIndex,r为它的右驱节点
for (Index<K,V> q = head, r = q.right, d;;) {
//如果它的右驱节点不为空
if (r != null) {
//获取它内部的node
Node<K,V> n = r.node;
//获取node的key值
K k = n.key;
//如果值为null,说明该节点正在被删除,帮助删除,然后再次获取HeadIndex新的右驱节点,重新从头执行
if (n.value == null) {
if (!q.unlink(r))
break; // restart
r = q.right; // reread r
continue;
}
//如果当前key>右驱节点的key,则向右遍历,向右移动一位继续从头比较
if (cpr(cmp, key, k) > 0) {
q = r;
r = r.right;
continue;
}
}
//如果q.down==null,则说明已经到底层
//并且此时q.key< key < r.key了,所以直接返回前驱节点q.node
if ((d = q.down) == null)
return q.node;
//向下走
q = d;
r = d.right;
}
}
}
接下来我们来看看get方法获取值的方法
private V doGet(Object key) {
if (key == null)
throw new NullPointerException();
Comparator<? super K> cmp = comparator;
outer: for (;;) {
//获取key的前驱节点,此时应该是n.key>=key的
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
Object v; int c;
//如果n不存在,则说明这个key对应的node不存在,直接返回null
if (n == null)
break outer;
Node<K,V> f = n.next;
//如果n!=b.next,说明有其他线程执行了插入操作,重新从头执行
if (n != b.next) // inconsistent read
break;
//n.value==null说明n已经被删除了,重新从头执行
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
//说明前驱节点已经被删除了,重新从头执行
if (b.value == null || v == n) // b is deleted
break;
//如果key和n.key相同,则返回n.value
if ((c = cpr(cmp, key, n.key)) == 0) {
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
//如果c<0,则说明key
if (c < 0)
break outer;
//走到这儿说明当前位置不准确,向后移动一位重新从头开始比较
b = n;
n = f;
}
}
return null;
}
然后我们来看下删除方法
final V doRemove(Object key, Object value) {
if (key == null)
throw new NullPointerException();
Comparator<? super K> cmp = comparator;
outer: for (;;) {
//获取key的前驱节点
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
Object v; int c;
//如果n=null,则说明不存在该key对应的元素,直接返回
if (n == null)
break outer;
Node<K,V> f = n.next;
//如果n!=b.next,说明有其他线程添加元素了,重新开始执行
if (n != b.next) // inconsistent read
break;
//如果n.value=null,说明n正在被删除,所以重新开始执行
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
//说明前驱节点b被删除了,重新开始执行
if (b.value == null || v == n) // b is deleted
break;
//如果key
if ((c = cpr(cmp, key, n.key)) < 0)
break outer;
//如果key>n.key,说明当前定位有偏差,向右移动一位,再继续执行
if (c > 0) {
b = n;
n = f;
continue;
}
//value传进来就是null,所以这个跳过
if (value != null && !value.equals(v))
break outer;
//将n的值设置为null,进行数据删除
if (!n.casValue(v, null))
break;
//这里会在n的后面添加一个Marker节点,将b和n的后继节点f链接,然后删除n
if (!n.appendMarker(f) || !b.casNext(n, f))
//对 key 对应的index 进行删除
findNode(key); // retry via findNode
//对 key 对应的index 进行删除 上一步进行操作失败后通过 findPredecessor 进行index 的删除
else {
findPredecessor(key, cmp); // clean index
//进行headIndex 对应的index 层的删除
if (head.right == null)
tryReduceLevel();
}
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
}
return null;
}
这里有个比较特别的地方,我们发现在删除的时候,会先在被删除的节点后面增加一个Marker节点。那么为什么要多此一举做这步操作呢?主要是为了防止误删除的情况发生,我们来看个例子:
例如有三个连续的节点ABC,线程1准备在B节点的后面插入一个节点 D, 它先判断 B.value == null, 发现 B 没被删除,这时线程2对B进行删除,线程2设置next为节点C成功删除B,这时候线程1设置B节点的后继节点为D,但是因为节点B被删除了,导致节点D虽然插入成功但是却找不到了。
但是如果增加了Marker节点就能很好的解决这个问题。
还是有三个连续的节点ABC,线程1准备在B节点的后面插入一个节点 D, 它先判断 B.value == null, 发现 B 没被删除,线程2对B节点进行删除,在B的后面增加一个MarkerNode M,然后线程2将节点B和M一起删除。这时候在线程1插入节点D过程中会出现如下几种情况:
maker 节点的存在致使非阻塞链表能实现中间节点的删除和插入同时安全进行(反过来就是若没有marker节点, 有可能刚刚插入的数据就丢掉了)。
除此之外还有一个并发容器为ConcurrentSkipListSet,他内部完全是使用的ConcurrentSkipListMap来实现,这里就不再细说。
【JUC】JDK1.8源码分析之ConcurrentSkipListMap
ConcurrentSkipListMap 源码分析 (基于Java 8)
Fork/Join框架是JDK1.7中提供的用于并行执行任务的框架,如名字所示,它的思想首先是Fork,把一个大任务分割成若干个小任务,然后进行Join,汇总每个小任务的结果得到最终任务结果。运行流程图如下:
上述过程Fork/Join框架提供了两个类让我们完成上述事情。
使用案例
我们先通过一个例子来看一下如何使用,计算1-1000的值。
public class ForkJoinDemo extends RecursiveTask<Integer>{
//阀值
private static final int THRESHOLD=100;
//起始结束值
private int start;
private int end;
public ForkJoinDemo(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum=0;
//如果条件成立,说明任务已经被分的足够小,开始执行计算逻辑
if(end-start<=THRESHOLD){
System.out.println("开始计算的部分:"+start+"-"+end);
for (int i=start;i<=end;i++){
sum+=i;
}
//否则,说明任务太大,将任务分割后执行
}else {
ForkJoinDemo subTask1=new ForkJoinDemo(start,(start+end)/2);
ForkJoinDemo subTask2=new ForkJoinDemo((start+end)/2+1,end);
subTask1.fork();
subTask2.fork();
sum=subTask1.join()+subTask2.join();
}
return sum;
}
public static void main(String[] args) {
ForkJoinDemo forkJoinDemo=new ForkJoinDemo(1,1000);
ForkJoinPool forkJoinPool=new ForkJoinPool();
ForkJoinTask<Integer> submit = forkJoinPool.submit(forkJoinDemo);
Integer result = null;
try {
result = submit.get();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(result);
}
}
ForkJoinTask需要实现compute方法,在这个方法里,首先要判断任务是否足够小,如果足够小就直接执行任务,否则就需要分割成两个子任务,然后每个子任务调用fork方法时,又会进入compute方法进行判断。使用join方法会等待子任务执行完并得到其结果。
ForkJoinPool内部由ForkJoinTask数组和ForkJoinWorkerThread数组组成。ForkJoinTask数组负责存放用户提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组则负责执行这些任务。