发布于2026-07-06 阅读(0)
扫一扫,手机访问
ConcurrentSkipListMap 是 Ja va 并发包里一个相当特别的存在——它是线程安全的,底层基于跳表(Skip List),天然保持键的有序性。这意味着什么?简单说,你不需要额外的锁,不需要手动排序,就能搞定高并发场景下的有序数据管理。平均时间复杂度是 O(log n),插入、查找、删除都稳得住。
它特别适合用来构建那种高并发、实时更新、按分数排序的全局排行榜。不需要外部排序逻辑,不需要担心线程安全,结构和性能都是现成的。
TreeMap 是有序的,这点不假,但它不是线程安全的。你当然可以用 Collections.synchronizedSortedMap 把它包起来,结果就是所有操作串行化,高并发下吞吐量直接崩盘。
ConcurrentSkipListMap 的设计则完全不同:内部采用了无锁(Lock-Free)和细粒度锁混合的策略。不同区段的数据可以并发操作,互不干扰。它还原生支持 putIfAbsent、replace、computeIfAbsent 这些原子操作,非常适合分数更新的竞争场景。迭代器是弱一致性的,遍历时不会抛 ConcurrentModificationException,分页拉取榜单非常顺手。再加上 subMap、headMap、tailMap 这些方法,获取 Top N 或指定区间排名,几行代码搞定。
排行榜绕不开一个经典问题——并列排名。两个用户都得 95 分,名次怎么算?如果直接用 score → userId 这种一对一映射,并列信息就丢了。
推荐的方案是:键(key)用分数(int/long),按升序或降序排列(默认升序,降序需要传入 Comparator.reverseOrder())。值(value)用线程安全的集合,比如 ConcurrentHashMap,或者更轻量的 ConcurrentSkipListSet,用来存该分数下的所有用户 ID。
举个例子:map.put(95, ConcurrentHashMap.newKeySet());,后面直接用 map.get(95).add("user123") 安全追加。简单,又清楚。
更新分数时,你得先查旧分,如果分数变了,从旧分数的桶里移除用户,再放入新分数的桶里。要注意的是,如果旧分数桶空了,可以顺手清理掉,避免内存累积。
// 假设 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); }
需要用降序构造 map:new ConcurrentSkipListMap<>(Comparator.reverseOrder())。然后直接取前 N 个条目就行。
public List>> getTopN(int n) { return new ArrayList<>(map.entrySet().stream() .limit(n) .collect(Collectors.toList())); }
因为同分并列,排名 = 「严格高于该分的用户总数」+ 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,每次 updateScore 时同步更新。这样查用户分数就是 O(1),不需要遍历整个排行榜。
• 内存控制:长期运行的系统,排行榜不能无限膨胀。可以在 updateScore 中检查总用户数,通过 map.descendingMap().skip(10000).forEach(...) 清理尾部低分数据。注意:descendingMap 返回的是视图,清理时要用原 map 的 keySet。
• 分数类型选择:分数用 Long 是稳妥的选择,防止溢出。如果涉及小数精度,用 BigDecimal 作 key,但必须提供严格全序的 Comparator,避免 NaN 或精度问题导致的诡异行为。
• 避免在循环中调用 computeIfAbsent:它的 lambda 可能会被多次执行,务必确保没有副作用。更稳妥的做法是先 get,再 putIfAbsent + replace 组合。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8