ConcurrentSkipListMap学习到跳表
之前写过几篇关于多线程的学习笔记。但是写的可能比较乱,且不是很详细。看后面如果有时间会重新拿来搞一遍,也算是巩固自己的知识体系吧。这里其实主要是是要学习跳表是算法。
ConcurrentSkipListMap
ConcurrentSkipListMap 多线程下安全的有序键值对。 ConcurrentSkipListMap是通过跳表来实现的。跳表是一个链表,但是通过使用“跳跃式”查找的方式使得插入、读取数据时复杂度变成了O(logn)。
跳表(Skip List)是一种**“空间换时间”**的算法设计思想。我们来通过一个例子来看下Skip List 是如何实现查找的。
首先我们按照重量大小排序的苹果100个(假设没有重量重复的苹果)。
如果是普通的链表要找到重量为3Kg的苹果。如果不是跳表结构我们从第一个开始称重,然后第二个直到找到重量是3Kg的。
如果是跳表则会有如下结构(暂时假设层级只有2)
1kg | 2.95kg | 3.5kg | 6.7kg | ||||||
---|---|---|---|---|---|---|---|---|---|
1kg | 1.1kg | 2.95kg | … | 3Kg | 3.5kg | … | 5kg | … | 6.7kg |
即可以理解为这100个苹果中有几个苹果是明确标签的标签上写着重量,但是我们要翻开标签才能看到。那么这个时候我们找3kg的苹果 的时候我们就可以这样做先翻开第一个标签发现是1kg,那么3kg肯定在后面。然后我们翻开第二个标签一看发现是2.95。3kg还在后面,我们继续翻标签找到第一个大于3的标签,那么3kg肯定在这个标签和上一个标签之间,我们只需要测量这中间的苹果即可找到3kg的苹果。这样查询效率就大大提高了。
层数是根据一种随机算法得到的,为了不让层数过大,还会有一个最大层数MAX_LEVEL限制,随机算法生成的层数不得大于该值。
在实际程序使用中标签肯定指会向一个下一层的一个苹果。且标签肯定是一个有序的,不然我们也无法快速定位了。
ConcurrentSkipListMap结构解析
主要结构
// 主要结构
private static final Object BASE_HEADER = new Object(); // 最底层链表的头指针BASE_HEADER
private transient volatile HeadIndex<K, V> head; // 最上层链表的头指针head
// 普通结点 Node 定义
static final class Node<K, V> {
final K key;
volatile Object value;
volatile Node<K, V> next;
// ...
}
// 头索引结点 HeadIndex
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;
}
}
// 索引结点 Index 定义
static class Index<K, V> {
final Node<K, V> node; // node指向最底层链表的Node结点
final Index<K, V> down; // down指向下层Index结点
volatile Index<K, V> right; // right指向右边的Index结点
// ...
}
Node:最底层链表中的结点,保存着实际的键值对,如果单独看底层链,其实就是一个按照Key有序排列的单链表。
HeadIndex:结点是各层链表的头结点,它是Index类的子类,唯一的区别是增加了一个
level
字段,用于表示当前链表的级别,越往上层,level值越大。Index:结点是除底层链外,其余各层链表中的非头结点(见示意图中的蓝色结点)。每个Index结点包含3个指针:
down
、right
、node
。down和right指针分别指向下层结点和后继结点,node指针指向其最底部的node结点。、
构造方法
// 构造方法
// 空Map.
public ConcurrentSkipListMap() {
this.comparator = null;
initialize();
}
// 指定比较器的构造器
public ConcurrentSkipListMap(Comparator<? super K> comparator) {
this.comparator = comparator;
initialize();
}
// 从给定Map构建的构造器
public ConcurrentSkipListMap(Map<? extends K, ? extends V> m) {
this.comparator = null;
initialize();
putAll(m);
}
// 从给定SortedMap构建的构造器、并且Key的顺序与原来保持一致.
public ConcurrentSkipListMap(SortedMap<K, ? extends V> m) {
this.comparator = m.comparator();
initialize();
buildFromSorted(m);
}
// 将一些字段置初始化null,然后将head指针指向新创建的HeadIndex结点。
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
// put public V put(K key, V value) { if (value == null) // ConcurrentSkipListMap的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 if (key == null) // key不能为空 throw new NullPointerException(); Comparator<? super K> cmp = comparator; // 找到插入节点位置 outer: for (;;) { for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { if (n != null) { Object v; int c; Node<K,V> f = n.next; // 集中情况会导致本次插入失败,会重新进入outer循环 // 1、 b.next被改动了 2、b.next被删掉了 3、b.next值变为空 if (n != b.next) // inconsistent read break; if ((v = n.value) == null) { // n is deleted n.helpDelete(b, f); break; } if (b.value == null || v == n) // b is deleted break; // 按照Key排序 if ((c = cpr(cmp, key, n.key)) > 0) { b = n; n = f; continue; } // 当插入的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); if (!b.casNext(n, z)) break; // restart if lost race to append to b break outer; } } // 判断是否需要扩层 // 0x80000001展开为二进制为10000000000000000000000000000001 // 这里(rnd & 0x80000001) == 0 // 相当于排除了负数(负数最高位是1),排除了奇数(奇数最低位是1) // 只有最高位最低位都不为1的数跟0x80000001做&操作才会为0,也就是正偶数 int rnd = ThreadLocalRandom.nextSecondarySeed(); if ((rnd & 0x80000001) == 0) { // test highest and lowest bits // 默认level为1,也就是只要到这里了就会至少建立一层索引 int level = 1, max; // 随机数从最低位的第二位开始,有几个连续的1则level就加几 while (((rnd >>>= 1) & 1) != 0) ++level; Index<K,V> idx = null; HeadIndex<K,V> h = head; // 新建一个目标节点的索引节点(Index) if (level <= (max = h.level)) { for (int i = 1; i <= level; ++i) idx = new Index<K,V>(z, idx, null); } else { // try to grow by one level // 如果最高层级大于原来的最高层级,则加一层索引 level = max + 1; // hold in array and later pick the one to use @SuppressWarnings("unchecked")Index<K,V>[] idxs = (Index<K,V>[])new Index<?,?>[level+1]; for (int i = 1; i <= level; ++i) idxs[i] = idx = new Index<K,V>(z, idx, null); for (;;) { // 原头节点 h = head; // 原头节点层级 int oldLevel = h.level; // 如果重新规划的层级小于或等于取出来的层级,说明头部节点被其他线程动过 // 因为进到这个esle就是要增加层级的,这里就不做后面的处理 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; } } } // 经过上面的步骤,有两种情况 // 一是没有超出高度,新建一条目标节点的索引节点链 // 二是超出了高度,新建一条目标节点的索引节点链,同时最高层头索引节点同样往上长 // find insertion points and splice in 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); if (n.value == null) { if (!q.unlink(r)) break; r = q.right; continue; } if (c > 0) { q = r; r = r.right; continue; } } if (j == insertionLevel) { if (!q.link(r, t)) break; // restart if (t.node.value == null) { findNode(key); break splice; } if (--insertionLevel == 0) break splice; } if (--j >= insertionLevel && j < level) t = t.down; q = q.down; r = q.right; } } } return null; } // 查找小于且最接近给定key的Node结点 private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) { if (key == null) throw new NullPointerException(); // don't postpone errors for (;;) { for (Index<K,V> q = head, r = q.right, d;;) { if (r != null) { Node<K,V> n = r.node; K k = n.key; if (n.value == null) { if (!q.unlink(r)) break; // restart r = q.right; // reread r continue; } // key大于k,继续向右查找 if (cpr(cmp, key, k) > 0) { q = r; r = r.right; continue; } } // 已经到了level1的层、直接返回对应的Node结点 if ((d = q.down) == null) return q.node; // 转到下一层,继续查找(level-1层) q = d; r = d.right; } } }
整个插入过程分成三个部分:
1、找到目标节点的位置并插入
找到离目标节点最近的索引节点对应的数据节点;
从这个数据节点开始往后遍历,直到找到目标节点应该插入的位置;(当插入的key已经有值了就覆盖)
2、随机决定是否需要建立索引及其层次,如果需要则建立自上而下的索引
看运气决定是否创建索引(rnd & 0x80000001 正偶数变化)
算出来的层级不高于现有最高层级,则直接建立一条竖直的索引链表(right没值),并结束此部分
算出来的层级高于现有最高层级,则新的层级为旧层级加1,同样建立一条竖直的索引链表(right没值),将头索引也向上增加到相应的高度,并结束此部分
3、将新建的索引节点(包含头索引节点)与其它索引节点通过右指针连接在一起(补上right指针)
从最高层级的头索引节点开始,向右遍历,找到目标索引节点的位置;
如果当前层有目标索引,则把目标索引插入到这个位置,并把目标索引前一个索引向下移一个层级;
如果当前层没有目标索引,则把目标索引位置前一个索引向下移一个层级;同样地,再向右遍历,寻找新的层级中目标索引的位置重复上面的步骤。
依次循环找到所有层级目标索引的位置并把它们插入到横向的索引链表中
-
remove & doRemove
在删除键值对时,不会立即执行删除,而是通过引入**“标记结点”,以“懒删除”**的方式进行,以提高并发效率。
public V remove(Object key) { return doRemove(key, null); } final V doRemove(Object key, Object value) { if (key == null) throw new NullPointerException(); Comparator<? super K> cmp = comparator; outer: for (;;) { // 寻找目标节点之前的最近的索引节点对应的数据节点 // 为了方便,这里叫b为当前节点,n为下一个节点,f为下下个节点 for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; // 和新增类似做几个检查 if (n == null) break outer; Node<K,V> f = n.next; if (n != b.next) // inconsistent read break; // 下个节点的值也为null了,说明有其它线程标记该元素为删除状态了 if ((v = n.value) == null) { // n is deleted n.helpDelete(b, f); break; } if (b.value == null || v == n) // b is deleted break; // 如果c<0,说明没找到元素,退出外层循环 if ((c = cpr(cmp, key, n.key)) < 0) break outer; // 如果c>0,说明还没找到,继续向右找 if (c > 0) { b = n; n = f; continue; } // c=0,说明n就是要找的元素 // 如果value不为空且不等于找到元素的value,不需要删除,退出外层循环 if (value != null && !value.equals(v)) break outer; if (!n.casValue(v, null)) // 如果删除失败,说明其它线程先一步修改了 break; // 到了这里n的值肯定是设置成null了 // 使用cas将n的下一个节点指向自己并且b的next为n的next(这里虽然用的|| 但要注意!) // 意思就是这里要保证要删除的节点的next指针指向自己,并且自己的上一个节点的指针要指向自己的next节点 if (!n.appendMarker(f) || !b.casNext(n, f)) // 使用这个方法尝试删除,其实就是最开始的校验最后调用的还是helpDelete() findNode(key); // retry via findNode else { // 处理索引 里面有一个unlink findPredecessor(key, cmp); // clean index // 如果最高层头索引节点没有右节点,则跳表的高度降级 if (head.right == null) tryReduceLevel(); } @SuppressWarnings("unchecked") V vv = (V)v; return vv; } } return null; } // Node.class中的方法,协助删除元素 void helpDelete(Node<K,V> b, Node<K,V> f) { /* * Rechecking links and then doing only one of the * help-out stages per call tends to minimize CAS * interference among helping threads. */ // 这里的调用者this==n,三者关系是b->n->f if (f == next && this == b.next) { // 将n的值设置为null后,会先把n的下个节点设置为marker节点 // 这个marker节点的值是它自己 // 这里如果不是它自己说明marker失败了,重新marker if (f == null || f.value != f) // not already marked casNext(f, new Node<K,V>(f)); else // marker过了,就把b的下个节点指向marker的下个节点 b.casNext(this, f.next); } }
1、寻找目标节点之前最近的一个索引对应的数据节点(数据节点都是在最底层的链表上);
2、从这个数据节点开始往后遍历,直到找到目标节点的位置;
3、如果这个位置没有元素,直接返回null,表示没有要删除的元素
4、如果这个位置有元素,先通过
n.casValue(v, null)
原子更新把其value设置为null;5、通过
n.appendMarker(f)
和b.casNext(n, f)
修改数据节点的指针6、通过
findPredecessor(key, cmp)
中的q.unlink(r)
删除索引节点;7、head的right指针指向了null,则跳表高度降级;
-
get
public V get(Object key) { return doGet(key); } private V doGet(Object key) { // key不为空 if (key == null) throw new NullPointerException(); Comparator<? super K> cmp = comparator; outer: for (;;) { // 寻找目标节点之前最近的索引对应的数据节点 // 为了方便,这里叫b为当前节点,n为下个节点,f为下下个节点 for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) { Object v; int c; // 常规检查 if (n == null) break outer; Node<K,V> f = n.next; if (n != b.next) // inconsistent read break; // 如果n的值为空,说明节点已被其它线程标记为删除 if ((v = n.value) == null) { // n is deleted // 协助删除,再重试 n.helpDelete(b, f); break; } // 如果b的值为空或者v等于n,说明b已被删除 // 这时候n就是marker节点,那b就是被删除的那个 if (b.value == null || v == n) // b is deleted break; // 如果c==0,说明找到了元素,就返回元素值 if ((c = cpr(cmp, key, n.key)) == 0) { @SuppressWarnings("unchecked") V vv = (V)v; return vv; } // 如果c<0,说明没找到元素 if (c < 0) break outer; // 如果c>0,说明还没找到,继续寻找 // 当前节点往后移 b = n; // 下一个节点往后移 n = f; } } return null; }
源码部分参考文章:
https://blog.csdn.net/weixin_30569033/article/details/96893761
https://mp.weixin.qq.com/s?__biz=MzkxNDEyOTI0OQ==&mid=2247484459&idx=1&sn=b4e7db9fdf256dfb312a2a90fcde79b3&source=41#wechat_redirect
Redis里面的跳表
参考文档:
https://www.cnblogs.com/mxxct/p/13857494.html
https://www.cnblogs.com/lfri/p/9991925.html
https://www.cnblogs.com/hunternet/p/11248192.html
redis 数据类型 zset 实现有序集合,底层使用的数据结构是跳表。因为redis是用C写的。我基本已经把上学时候学的C都还给老师了,这里就不看源码了。看上面的几篇博客也就理解了。
这里搬运下上面博客中的几句话:
为什么使用跳跃表,而不是平衡树等用来做有序元素的查找
-
跳跃表的时间复杂度和红黑树是一样的,而且实现简单
-
在并发的情况下,红黑树在插入删除的时候可能需要做rebalance的操作,这样的操作可能会涉及到整个树的其他部分;而链表的操作就会相对局部,只需要关注插入删除的位置即可,只要多个线程操作的地方不一样,就不会产生冲突