ConcurrentSkipListMap和ConcurrentHashMap

ConcurrentSkipListMap和ConcurrentHashMapConcurrentSk 使用红黑树按照 key 的顺序 自然顺序 自定义顺序 来使得键值对有序存储 但是只能在单线程下安全使用 多线程下想要使键值对按照 key 的顺序来存储 则需要使用 ConcurrentSk ConcurrentSk 的底层是通过跳表来实现的 跳表是一个链表 但是通过使用 跳跃式 查找的方式使得插入 读取数据时复杂度变成了 O logn 跳表 SkipList 使用 空间换时间 的算法 令链表的每个结点不仅记录 next

ConcurrentSkipListMap
TreeMap使用红黑树按照key的顺序(自然顺序、自定义顺序)来使得键值对有序存储,但是只能在单线程下安全使用;多线程下想要使键值对按照key的顺序来存储,则需要使用ConcurrentSkipListMap。

ConcurrentSkipListMap的底层是通过跳表来实现的。跳表是一个链表,但是通过使用“跳跃式”查找的方式使得插入、读取数据时复杂度变成了O(logn)。

跳表(SkipList):使用“空间换时间”的算法,令链表的每个结点不仅记录next结点位置,还可以按照level层级分别记录后继第level个结点。在查找时,首先按照层级查找,比如:当前跳表最高层级为3,即每个结点中不仅记录了next结点(层级1),还记录了next的next(层级2)、next的next的next(层级3)结点。现在查找一个结点,则从头结点开始先按高层级开始查:head->head的next的next的next->。。。直到找到结点或者当前结点q的值大于所查结点,则此时当前查找层级的q的前一节点p开始,在p~q之间进行下一层级(隔1个结点)的查找…直到最终迫近、找到结点。此法使用的就是“先大步查找确定范围,再逐渐缩小迫近”的思想进行的查找。

例如:有当前的跳表存储如下:有4个层级,层级1为最下面的level,是一个包含了所有结点的普通链表。往上数就是2,3,4层级。

现在,我们查找结点值为19的结点:

ConcurrentSkipListMap和ConcurrentHashMap

ConcurrentSkipListMap和ConcurrentHashMap
明白了查找的原理后,插入、删除就容易理解了。为了保存跳表的有序性,所以分三步:查找合适位置——进行插入/删除——更新跳表指针,维护层级性。

插入结点:

ConcurrentSkipListMap和ConcurrentHashMap

删除结点:

ConcurrentSkipListMap和ConcurrentHashMap

知道了底层所用数据结构的原理后,我们来看看concurrentskiplistmap的部分源码:

插入:

private Node

findNode(Comparable
key) {


    for (;;) {

        // 找到key的前继节点
        Node

b = findPredecessor(key);

        // 设置n为“b的后继节点”(即若key存在于“跳表中”,n就是key对应的节点)
        Node

n = b.next;

        for (;;) {

            // 如果“n为null”,则跳转中不存在key对应的节点,直接返回null。
            if (n == null)
                return null;
            Node

f = n.next;

            // 如果两次读取到的“b的后继节点”不同(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
            if (n != b.next)                // inconsistent read
                break;
            Object v = n.value;
            // 如果“当前节点n的值”变为null(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
            if (v == null) {                // n is deleted
                n.helpDelete(b, f);
                break;
            }
            if (v == n || b.value == null)  // b is deleted
                break;
            // 若n是当前节点,则返回n。
            int c = key.compareTo(n.key);
            if (c == 0)
                return n;
            // 若“节点n的key”小于“key”,则说明跳表中不存在key对应的节点,返回null
            if (c < 0)
                return null;
            // 若“节点n的key”大于“key”,则更新b和n,继续查找。
            b = n;
            n = f;
        }
    }
}

通过上面的源码可以发现:ConcurrentSkipListMap线程安全的原理与非阻塞队列ConcurrentBlockingQueue的原理一样:利用底层的插入、删除的CAS原子性操作,通过死循环不断获取最新的结点指针来保证不会出现竞态条件。







































ConcurrentHashMap
特此说明 :
本文concurrentHashMap是jdk1.7中的实现,jdk1.8中使用的不是Segment

快速存取

键值对使用HashMap;多线程并发存取

键值对使用ConcurrentHashMap;

我们知道,HashTable和和Collections类中提供的同步HashTable是线程安全的,但是他们线程安全是通过在进行读写操作时对整个map加锁来实现的,故此性能比较低。那既然是由于锁粒度(加锁的范围叫锁粒度)太大造成的性能低下,可不可以从锁粒度着手去改良呢?由此,就引申出了ConcurrentHashMap。

ConcurrentHashMap采取了“锁分段”技术来细化锁的粒度:把整个map划分为一系列被成为segment的组成单元,一个segment相当于一个小的hashtable。这样,加锁的对象就从整个map变成了一个更小的范围——一个segment。

ConcurrentHashMap线程安全并且提高性能原因就在于:对map中的读是并发的,无需加锁;只有在put、remove操作时才加锁,而加锁仅是对需要操作的segment加锁,不会影响其他segment的读写,由此,不同的segment之间可以并发使用,极大地提高了性能。

结构分析

ConcurrentSkipListMap和ConcurrentHashMap
Segment的结构:

static final class Segment

extends ReentrantLock implements Serializable {


    transient volatile int count;
    transient int modCount;
    transient int threshold;
    transient volatile HashEntry

[] table;

    final float loadFactor;
}

count:Segment中元素的数量,用于map.size()时统计整个map的大小使用
modCount:对table的大小造成影响的操作的数量(比如put或者remove操作),用于统计size时验证结果的正确性
threshold:阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容,concurrenthashmap自身不会扩容(segment的数量在map创建后不会再增加,在容量不足时只会增加segment的容量)
table:链表数组,数组中的每一个元素代表了一个链表的头部,一个链表用于存储相同hash值的不同元素们
loadFactor:负载因子,用于确定threshold,决定扩容的时机
查询














static final class HashEntry

{


    final K key;
    final int hash;
    volatile V value;
    final HashEntry

next;

}






final Segment

segmentFor(int hash) {


    return segments[(hash >>> segmentShift) & segmentMask];
}


HashEntry

getFirst(int hash) {


    HashEntry

[] tab = table;

    return tab[hash & (tab.length – 1)];
}

由上面可以看到:concurrenthashmap的查询操作经过三步:第一次hash确定key在哪个segment中;第二次hash在segment中确定key在链表数组的哪个链表中;第三步遍历这个链表,调用equals()进行比对,找到与所查找key相等的结点并读取。






插入

删除

segment的链表数组中的链表结构如下:

static final class HashEntry

{


    final K key;
    final int hash;
    volatile V value;
    final HashEntry

next;

}

我们可以看到,链表中结点只有value是可修改的,因此,如果我们需要删除结点时,是不能简单地由前继结点指向被删结点的后继结点来实现。所以,我们只能重构链表。








统计大小—Size()

统计整个map的大小时,如果在统计过程中把整个map锁住,则会造成影响读写。ConcurrentHashMap通过采用segment中的属性成员来优化这个过程。

static final class Segment

extends ReentrantLock implements Serializable {


    transient volatile int count;
    transient int modCount;
   ….
}

我们看到,每个segment中有一个count记录当前segment的元素数量,每当put/remove成功就会把这个值+1/-1。因此,在统计map的大小时,我们把每个segment的count加起来就是了。但是,如果在加的过程中,发生了修改怎么办呢?比如:把segment[2]的count加到total后,segment[2]发生了remove操作,这样就会造成统计结果不正确。此时就需要用modCount,modCount记录了segment的修改次数,这个值只增不减,无论是插入、删除都会导致该值+1.






ConcurrentHashMap在统计size时,经历了两次遍历:第一次不加锁地遍历所有segment,统计count和modCount的总和得到C1和M1;然后再次不加锁地遍历,得到C2和M2,比较M1和M2,如果修改次数没有发生变化则说明两次遍历期间map没有发生数量变化,那么C1就是可用的。如果M1不等于M2,则说明在统计过程中map的数量发生了变化,此时才采取最终手段——锁住整个map进行统计。

但ConcurrentSkipListMap有几个ConcurrentHashMap 不能比拟的优点:

1、ConcurrentSkipListMap 的key是有序的。

2、ConcurrentSkipListMap 支持更高的并发。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/220744.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月17日 下午7:45
下一篇 2026年3月17日 下午7:46


相关推荐

  • Java面试题–较经典

    Java面试题–较经典1、出处:2016年360Java面试题:原题:首先 代码跑一边 保证正确性。分析:往方法中传参,传的仅仅只是地址,而不是实际内存,所以不要以为y=x程序的执行,是 b=a的执行。这两者是不相等的。 2、出处:2016年 阿里巴巴Java面试题:原题:分析:本题是一个自动拆装箱的考题(自动拆装箱JDK需在1.5上)参考:https://blog….

    2022年6月13日
    36
  • 端口 TCP/IP =PORT NUMBERS[通俗易懂]

    端口 TCP/IP =PORT NUMBERS[通俗易懂] http://www.iana.org/assignments/port-numbersPORTNUMBERS(lastupdated2009-10-28)Theportnumbersaredividedintothreeranges:theWellKnownPorts,theRegisteredPorts,andtheDynamic

    2026年4月15日
    5
  • clion 2022.01.13 激活码【最新永久激活】

    (clion 2022.01.13 激活码)这是一篇idea技术相关文章,由全栈君为大家提供,主要知识点是关于2021JetBrains全家桶永久激活码的内容IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/100143.htmlLGWSVFD4PZ-eyJsaWN…

    2022年4月1日
    211
  • ActionContext_activity和action的区别

    ActionContext_activity和action的区别

    2025年10月15日
    5
  • oracle 分页查询语句

    oracle 分页查询语句select from selectREC ROWNUMRNfrom selectt fromtablet RECwhereROWN lt num2 whereRN gt num1 若果需要对结果集进行排序 可这样写 select from

    2026年3月19日
    1
  • hadoop的简介_hadoop体系

    hadoop的简介_hadoop体系一、概述Hadoop起源:hadoop的创始者是DougCutting,起源于Nutch项目,该项目是作者尝试构建的一个开源的Web搜索引擎。起初该项目遇到了阻碍,因为始终无法将计算分配给多台计算机。谷歌发表的关于GFS和MapReduce相关的论文给了作者启发,最终让Nutch可以在多台计算机上稳定的运行;后来雅虎对这项技术产生了很大的兴趣,并组建了团队开发,从Nutch中剥离出分布式计算模块命名为“Hadoop”。最终Hadoop在雅虎的帮助下能够真正的处理海量的Web数据。…

    2022年10月17日
    3

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号