Java并发进阶系列:深度讨论高并发跳表数据结构ConcurrentSkipListMap的源代码实现(下)

文章说明:因为CSM解析内容较多,因此全文分为“深度讨论高并发跳表数据结构ConcurrentSkipListMap的源代码实现(上)”和“深度讨论高并发跳表数据结构ConcurrentSkipListMap的源代码实现(下)”两篇文章
上篇:CSM数据结构设计原理、doGet、doPut核心方法解析
下篇:doRemove核心方法解析、总结

remove方法:
删除操作的设计原理

这里先介绍Doug Lea在源代码注释给出算法设计说明:n.helpDelete(b,f)的设计原理

上一篇文章的doGet、doPut方法中都有涉及到遇到被标记”删除“的节点时都会加入n.helpDelete(b,f)的处理逻辑,

     * In addition to using deletion markers, the lists also use
     * nullness of value fields to indicate deletion, in a style
     * similar to typical lazy-deletion schemes.  If a node's value is
     * null, then it is considered logically deleted and ignored even
     * though it is still reachable. This maintains proper control of
     * concurrent replace vs delete operations -- an attempted replace
     * must fail if a delete beat it by nulling field, and a delete
     * must return the last non-null value held in the field. (Note:
     * Null, rather than some special marker, is used for value fields
     * here because it just so happens to mesh with the Map API
     * requirement that method get returns null if there is no
     * mapping, which allows nodes to remain concurrently readable
     * even when deleted. Using any other marker value here would be
     * messy at best.)
     * n是当前要删除的节点,b是n的前驱节点,f是n的后继节点,开始删除前,(b,n,f)的指向关系如下
     * Here's the sequence of events for a deletion of node n with
     * predecessor b and successor f, initially:
     *
     *        +------+       +------+      +------+
     *   ...  |   b  |------>|   n  |----->|   f  | ...
     *        +------+       +------+      +------+
     *
     * 1. CAS n's value field from non-null to null.
     *    From this point on, no public operations encountering
     *    the node consider this mapping to exist. However, other
     *    ongoing insertions and deletions might still modify
     *    n's next pointer.
     *
     * 1、 将删除节点n的value使用cas设成null,表示此刻起,n节点处于待删除状态,虽然其value为null,对于public相关方法如get等,在执行遇到此类型节点时不会将该节点视为有效节点(也即会被跳过处理),但是如果对于正在执行的插入和删除操作的方法来说,可能也可以去更改“此待删除节点n”的后继节点。
     * 2. CAS n's next pointer to point to a new marker node.
     *    From this point on, no other nodes can be appended to n.
     *    which avoids deletion errors in CAS-based linked lists.
     *
     *        +------+       +------+      +------+       +------+
     *   ...  |   b  |------>|   n  |----->|marker|------>|   f  | ...
     *        +------+       +------+      +------+       +------+
     * 2、使用cas将n节点的next字段指向一个marker节点后,从此刻起,任何节点都不会被放在n的后继节点位置,也即不可能出现n->n1、n->n2等,只有n->marker
     * 3. CAS b's next pointer over both n and its marker.
     *    From this point on, no new traversals will encounter n,
     *    and it can eventually be GCed.
     *        +------+                                    +------+
     *   ...  |   b  |----------------------------------->|   f  | ...
     *        +------+                                    +------+
     * 3、通过cas将b的next指针(越过n节点以及n的后继marker节点)指向f节点,从此刻起,n节点不会被相关操作遍历到,n最终会被GC。

可以看出CSM删除操作方面,借用marker节点来实现,将待删除节点的value设为null值来表示该节点处在删除状态但还未真正删除,这种设计风格就像“惰性删除语义(lazy-deletion schemes)”,先标记删,等某个时机再真正执行之。如果CSM的一个节点的value是null,说明该节点在逻辑上已经被删除。

以下是remove方法的源代码解析

    public V remove(Object key) {
   
   
        return doRemove(key, null);//注意只需要比较key即可,不需要考虑value相等才删除!
    }
具体逻辑由doRemove实现
    final V doRemove(Object key, Object value) {
   
   
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
   
   
        		// 1、和doGet、findNode类似设计,首先找到key的前驱节点
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
   
   
            // 
                Object v; int c;
                if (n == null)
                    break outer;
              // 经典的(b,n,f)三指针
                Node<K,V> f = n.next;
              // 2、n已不是b的后继节点,也即读n节点前后不一致,则重试
                if (n != b.next)                    // inconsistent read
                    break;
              // 3、数据节点n节点被标记为删除状态,那么使用helpDelete把n节点删除,然后重试。
                if ((v = n.value) == null) {
   
           // n is deleted
                    n.helpDelete(b, f);
                    break;
                }
              // 4、key的前驱节点b被标记删除状态,只能重试,读取新的b
                if (b.value == null || v == n)      // b is deleted
                    break;
                //5、 给定的key不在数据链表里面,直接结束
                if ((c = cpr(cmp, key, n.key)) < 0)
                    break outer;
              	//6、 给定的key比当前数据节点n还大,那么更新b、n向右继续检索
                if (c > 0) {
   
   
                    b = n;
                    n = f;
                    continue;
                }
              	// 以下找到与key相等的n节点
                //7、doRemove的入参value默认是null,因此以下会被跳过,若指定value,则还需判断给定的value和当前找到n.value是否相等
                if (value != null && !value.equals(v))
                    break outer;
                //8、执行流运行到这里,说明满足删除n节点的条件也即key=n.key,n就是要删除的目标数据节点,因此将n.value设为null,cas失败则重试,注意这是是标记删除,不是直接把n节点删除。
                if (!n.casValue(v, null))
                    break;
                /*9、执行流运行到这里,说明n成功被标记删除状态
              	条件1:将 b → n → f 变成  b → n → marker → f
              	条件2:将 b → n → f 变成 b → f
              	如果条件1CAS失败或者条件2CAS失败,都会调用findNode(key)来删除数据节点n
              	*/
                if (!n.appendMarker(f) || !b.casNext(n, f))
                    findNode(key);                  // retry via findNode
                else {
   
   
                /*10、不妨假设条件1成立,也即将 b → n → f 变成  b → n → marker → f
                  需要n节点上方的索引节点都清除(包括清除索引节点的前后指向关系),恰好findPredecessor就是在索引层干这事。
                  */ 
                    findPredecessor(key, cmp);      // clean index
                    if (head.right == null)
                        tryReduceLevel();
                }
                @SuppressWarnings("unchecked") V vv = (V)v;
                return vv;
            }
        }
        return null;
    }

虽然在doGet方法中有给出findPredecessor,它是用在查询场景中,而在本小节中findPredecessor被用来删除n节点对应的上方索引节点场景中,现在结合上方doRmove的源代码理解,将更能掌握其中设计逻辑。

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;
                // 此处对应的是上方doRemove内部将n.value标记为null的情况
                if (n.value == null) {
   
   
                  	/* 既然数据层的n节点被删除,那么n节点对应上方关联的索引节点r也要删除:
                    索引层:将 q → r → r.right 变成  q → r.right 
                    */
                    if (!q.unlink(r))
                        break;           // restart
                  	// 索引节点r成功删除后,将r指向q的新right节点,此时q → r 两个索引节点都处于正常状态,继续下一轮遍历。
                    r = q.right;         // reread r
                    continue;
                }
              	// 继续在该层索引向右检索,直到cpr(cmp, key, k) =0
                if (cpr(cmp, key, k) > 0) {
   
   
                    q = r;
                    r = r.right;
                    continue;
                }
            }
          	// 当q位于第1层索引层位置时,此时q.down指向的是idx=null,因此可退出,到此从入口head到出口q.down=null沿途找到的与n对应的每层索引节点idx都被删除掉。可直接退出
            if ((d = q.down) == null) 
                return q.node;
          	// 执行流运行到这里,说明还未到达第1层索引层,以上逻辑完成当前层的idx节点删除,那么继续需要处理n节点对应的更低层的索引节点idx。
            q = d;
            r = d.right;
        }
    }
}

因此在doRemove方法的角度来看, findPredecessor(key, cmp) 实际意义是单纯用于clean index,而不是为key找到前驱节点b这么简单,这里的clean index功能就是Doug Lea提到的“side-effect”,也即下面这句话的含义:

Callers rely on this side-effect of clearing indices to deleted nodes.

doRemove作为调用方,能够从调用findPredecessor过程获得额外收益:清除那些已被标记为“删除状态”的数据节点上方对应的每层索引节点idx。

关于doRemove的图解这里不再给出,想要深入理解的同学务必自行将删除逻辑对应的图做出来,否则将难以理解其过程的动态处理。

tryReduceLevel()方法

在doRemove方法的第10个逻辑中,清理完索引节点后,还需要检查是否需要“降层处理”:

                else {
   
   
                /*10、不妨假设条件1成立,也即将 b → n → f 变成  b → n → marker → f
                  需要n节点上方的索引节点都清除(包括清除索引节点的前后指向关系),恰好findPredecessor就是在索引层干这事。
                  */ 
                    findPredecessor(key, cmp);      // clean index
                    if (head.right == null)
                        tryReduceLevel();
                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值