商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 如何在 Java 中利用 ConcurrentSkipListMap 实现一个天然支持高并发且有序的全局排行榜

如何在 Java 中利用 ConcurrentSkipListMap 实现一个天然支持高并发且有序的全局排行榜

  发布于2026-07-06 阅读(0)

扫一扫,手机访问

ConcurrentSkipListMap 是 Ja va 并发包里一个相当特别的存在——它是线程安全的,底层基于跳表(Skip List),天然保持键的有序性。这意味着什么?简单说,你不需要额外的锁,不需要手动排序,就能搞定高并发场景下的有序数据管理。平均时间复杂度是 O(log n),插入、查找、删除都稳得住。

它特别适合用来构建那种高并发、实时更新、按分数排序的全局排行榜。不需要外部排序逻辑,不需要担心线程安全,结构和性能都是现成的。

为什么 ConcurrentSkipListMap 比 TreeMap + synchronized 更适合排行榜?

TreeMap 是有序的,这点不假,但它不是线程安全的。你当然可以用 Collections.synchronizedSortedMap 把它包起来,结果就是所有操作串行化,高并发下吞吐量直接崩盘。

ConcurrentSkipListMap 的设计则完全不同:内部采用了无锁(Lock-Free)和细粒度锁混合的策略。不同区段的数据可以并发操作,互不干扰。它还原生支持 putIfAbsentreplacecomputeIfAbsent 这些原子操作,非常适合分数更新的竞争场景。迭代器是弱一致性的,遍历时不会抛 ConcurrentModificationException,分页拉取榜单非常顺手。再加上 subMapheadMaptailMap 这些方法,获取 Top N 或指定区间排名,几行代码搞定。

核心设计:用「分数 → 用户集合」映射解决并列排名

排行榜绕不开一个经典问题——并列排名。两个用户都得 95 分,名次怎么算?如果直接用 score → userId 这种一对一映射,并列信息就丢了。

推荐的方案是:键(key)用分数(int/long),按升序或降序排列(默认升序,降序需要传入 Comparator.reverseOrder())。值(value)用线程安全的集合,比如 ConcurrentHashMap,或者更轻量的 ConcurrentSkipListSet,用来存该分数下的所有用户 ID。

举个例子:map.put(95, ConcurrentHashMap.newKeySet());,后面直接用 map.get(95).add("user123") 安全追加。简单,又清楚。

关键操作实现(附线程安全写法)

1. 更新用户分数(含并列处理)

更新分数时,你得先查旧分,如果分数变了,从旧分数的桶里移除用户,再放入新分数的桶里。要注意的是,如果旧分数桶空了,可以顺手清理掉,避免内存累积。

// 假设 map: ConcurrentSkipListMap>
public void updateScore(String userId, int newScore) {
    // 先查旧分(如果存在)
    Integer oldScore = getUserScore(userId); // 需自行维护反向索引或扫描,见下文优化
    if (oldScore != null && oldScore != newScore) {
        map.get(oldScore).remove(userId); // 安全移除
        if (map.get(oldScore).isEmpty()) {
            map.remove(oldScore); // 清理空桶(可选,避免内存累积)
        }
    }
    // 加入新分
    map.computeIfAbsent(newScore, k -> ConcurrentHashMap.newKeySet()).add(userId);
}

2. 获取 Top K 排行榜(按分降序)

需要用降序构造 map:new ConcurrentSkipListMap<>(Comparator.reverseOrder())。然后直接取前 N 个条目就行。

public List>> getTopN(int n) {
    return new ArrayList<>(map.entrySet().stream()
            .limit(n)
            .collect(Collectors.toList()));
}

3. 查询用户当前排名(需支持「并列名次」)

因为同分并列,排名 = 「严格高于该分的用户总数」+ 1。利用 map.headMap(targetScore, false).values() 求和即可。

public int getRank(String userId) {
    Integer score = getUserScore(userId); // 此处需高效反查,见下文
    if (score == null) return -1;
    // sum 所有 > score 的用户数(因 map 降序,headMap(score, false) = 分数 > score 的子图)
    return map.headMap(score, false).values().stream()
            .mapToInt(Set::size)
            .sum() + 1;
}

性能优化与注意事项

• 反向索引加速用户查分:单独维护一个 ConcurrentHashMap userToScore,每次 updateScore 时同步更新。这样查用户分数就是 O(1),不需要遍历整个排行榜。

• 内存控制:长期运行的系统,排行榜不能无限膨胀。可以在 updateScore 中检查总用户数,通过 map.descendingMap().skip(10000).forEach(...) 清理尾部低分数据。注意:descendingMap 返回的是视图,清理时要用原 map 的 keySet。

• 分数类型选择:分数用 Long 是稳妥的选择,防止溢出。如果涉及小数精度,用 BigDecimal 作 key,但必须提供严格全序的 Comparator,避免 NaN 或精度问题导致的诡异行为。

• 避免在循环中调用 computeIfAbsent:它的 lambda 可能会被多次执行,务必确保没有副作用。更稳妥的做法是先 get,再 putIfAbsent + replace 组合。

本文转载于:https://www.php.cn/faq/2447411.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注