高性能集合与实现细节(快速导览)

举报
喵手 发表于 2026/01/15 17:44:43 2026/01/15
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

正文

HashMap(Java 8+)

  • 数据结构:内部是 Node<K,V>[] table(数组) + 每个槽位链表,链表长度超过 TREEIFY_THRESHOLD(默认 8)会 treeify 成 TreeNode(红黑树)。

  • 索引计算:用 hash = key == null ? 0 : spread(key.hashCode())index = (table.length - 1) & hash(所以 table 长度为 2 的幂可使位运算映射均匀)。

    • hash spread 通常为 h ^ (h >>> 16),把高位扰动到低位,减少低位碰撞。
  • 扩容策略:初始容量 capacity(默认 16),负载因子 loadFactor(默认 0.75)。扩容时 newCapacity = oldCapacity << 1(翻倍);threshold = capacity * loadFactor

  • 复杂度:期望情况下 put/get 为 O(1),最坏情况 O(n)(链表转成树后最坏 O(log n))。

  • 常见误用

    • 使用非常小的初始容量或错误负载因子导致频繁 resize(开销大)。
    • 用不可变性差的 hashCode() 导致大量碰撞。

ConcurrentHashMap(Java 8 实现要点)

  • 设计要点

    • Java 8 从分段(Segment)转向基于 CAS + synchronized 的 bin-level 并发控制(更细粒度)。
    • table 仍是 Node<K,V>[]。在插入时使用 CAS 初始化槽位;写入大多数时候使用 synchronized 在某个 bin 上(或对 treeBin)保护;读操作使用 volatile / CAS /无锁读取,通常不加锁。
    • resize 使用 ForwardingNode 标记并由多个线程协作推进(transfer)。
  • 并发语义

    • 不支持对整个 map 的原子复合操作(除非使用 compute/computeIfAbsent 等原子方法)。
    • computeIfAbsent 的 mappingFunction 在并发情况下可能被多次计算(取决于并发冲突),需保证幂等或轻量。
  • 优势/适用场景:高并发读多写少场景;比 Collections.synchronizedMap 更高吞吐。

LongAdder / LongAccumulator(高并发计数)

  • 原理:用“分片计数”(cells 数组 + base)来减少 CAS 竞争,线程先尝试更新 base,失败则退到 cells 的某个槽(依线程哈希)再 CAS 更新;最终读取时把各个 cell 值与 base 合并。
  • 适用:高并发下的计数器(例如 QPS、命中计数等),比 AtomicLong 性能好得多,但读取时稍贵(合并多个分片)。
  • 注意:不适合需要严格精确即时一致性的场景(合并时是一致性的近似快照)。

ArrayList / ArrayDeque / ArrayBlocking

  • ArrayList

    • 底层 Object[] elementData,动态扩容策略 newCapacity = old + (old >> 1)(即 1.5 倍)。
    • 随机访问 O(1),在尾部添加 amortized O(1),中间插入/删除 O(n)(需要移动数组)。
    • 初始为空数组优化(DEFAULTCAPACITY_EMPTY_ELEMENTDATA)。
  • ArrayDeque

    • 用循环数组实现双端队列(head/tail 索引),容量为 2 的幂,操作不使用边界检查(性能高)。
    • 不允许 null 元素;非常适合作为栈/队列的高性能替代(比 LinkedList 更快)。
  • 何时用链表? LinkedList 除非需要频繁在中间节点插入并已持有节点引用,否则通常不推荐(内存和缓存友好性差)。

内存布局与缓存友好优化(高级)

  • 对象头 + 引用开销:Java 对象有对象头(Mark Word + Klass pointer)和字段,引用(4/8 字节,取决于 compressed OOPs)导致“指针跳转”缓存不友好。

  • 优化建议

    • 使用 Array of primitives(int[], long[])而不是 List<Integer>` 等会避免装箱。
    • 使用 long[]byte[]ByteBuffer 做结构化存储以减少对象数量。
    • 对内存布局敏感时,将常用字段放在对象前面(热字段靠近),减少填充与对齐浪费。
    • 必要时考虑 off-heap(DirectByteBuffer、Unsafe / VarHandle 或第三方 off-heap 库)以减少 GC 压力,但引入复杂性。
    • 使用 primitive-specialized collections(fastutil、Eclipse Collections、Trove)避免频繁装箱——能显著降低内存与 CPU。

并发集合的扩展策略(设计思想)

  • 减少争用

    • 分段或分片(sharding):把 key-space 划分成多个子-map(ConcurrentHashMap 已内建),或手工用 ConcurrentHashMap 数组来进一步分片。
    • 读优先无锁:读操作尽可能无锁(volatile、CAS),写操作在小范围同步。
    • 批量/异步处理:把写入或日志上报异步化,减少临界区时长。
  • 幂等与重试:并发下数据操作需考虑幂等与重复执行(例如 computeIfAbsent mapping 需可安全重复执行)。

  • 可伸缩 eviction:缓存驱逐最好不要把整个结构上大锁(单点瓶颈),用近似算法或后台异步清理来保证吞吐。

实战练习:实现高性能缓存(ConcurrentHashMap + 近似 LRU 驱逐)

下面给出一个简单、并发友好的近似 LRU 缓存实现(适合高并发读取/写入、对精确 LRU 要求不是极端严格的场景)。要点:使用 ConcurrentHashMap 存储条目,使用 ConcurrentLinkedDeque 跟踪访问顺序(近似 LRU),并用 AtomicInteger 跟踪大小;当超过最大容量时异步或同步驱逐最老键。实现简单、性能好但允许访问队列出现重复条目(会在后续轮次清理),这是常见的折衷。

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * ApproximateConcurrentLRUCache:近似 LRU,适用于高并发场景(非严格 LRU)。
 * 优点:读取/写入路径无全局锁;驱逐为近似,代价低。
 * 缺点:访问队列允许重复 key(需要定期清理),不是严格 LRU。
 */
public class ApproximateConcurrentLRUCache<K, V> {
    private final ConcurrentHashMap<K, V> map;
    private final ConcurrentLinkedDeque<K> accessDeque;
    private final int maxSize;
    private final AtomicInteger size;
    private final ScheduledExecutorService cleaner; // 可选后台清理

    public ApproximateConcurrentLRUCache(int maxSize) {
        this.maxSize = maxSize;
        this.map = new ConcurrentHashMap<>(Math.min(16, maxSize));
        this.accessDeque = new ConcurrentLinkedDeque<>();
        this.size = new AtomicInteger(0);
        this.cleaner = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread t = new Thread(r, "cache-cleaner");
            t.setDaemon(true);
            return t;
        });
        // 定期清理重复的陈旧 key(避免队列无限膨胀)
        cleaner.scheduleAtFixedRate(this::trimToSize, 1, 1, TimeUnit.SECONDS);
    }

    public V get(K key) {
        V val = map.get(key);
        if (val != null) {
            accessDeque.addLast(key); // 记录一次访问(允许重复)
        }
        return val;
    }

    public void put(K key, V value) {
        V prev = map.put(key, value);
        if (prev == null) {
            int cur = size.incrementAndGet();
            accessDeque.addLast(key);
            if (cur > maxSize) {
                // 触发清理(可以选择异步)
                trimToSize();
            }
        } else {
            // 覆盖旧值:也把 key 推到访问队列末尾
            accessDeque.addLast(key);
        }
    }

    private void trimToSize() {
        while (size.get() > maxSize) {
            K oldest = accessDeque.pollFirst();
            if (oldest == null) break;
            V removed = map.remove(oldest);
            if (removed != null) {
                size.decrementAndGet();
            }
            // 若 removed == null,说明队列中是重复 key(已被其他线程更新/移除),继续循环
        }
    }

    public V remove(K key) {
        V removed = map.remove(key);
        if (removed != null) size.decrementAndGet();
        return removed;
    }

    public void shutdown() {
        cleaner.shutdownNow();
    }

    // 额外方法:size(), containsKey(), clear() 等根据需要添加
}

说明 & 优化点

  • ConcurrentLinkedDequeaddLast/pollFirst 是无锁并发操作;但队列允许重复 key,因此我们在驱逐时检查 map.remove(oldest) 是否真正移除条目。
  • 这是常见的“近似 LRU”实现(低开销)。若需要严格 LRU(精确且线程安全),推荐使用 LinkedHashMap + 单锁(在低并发下可行)或直接使用优秀现成库:Caffeine(目前生产级高性能缓存库)——它提供多种驱逐策略、异步加载、统计、权重等。
  • 驱逐策略也可用 TTL(定时过期)或基于权重的策略代替纯 LRU。

常见陷阱与性能评测建议

  • 误用 synchronized/map 的全表锁Collections.synchronizedMap(new HashMap()) 在高并发写场景下是瓶颈(单点锁)。用 ConcurrentHashMap 或分片替代。
  • 装箱/拆箱导致的性能问题:避免 List<Integer>Map<Long, X> 在热点场景频繁装箱,改用 primitive-specialized collections 或 long/Int arrays。
  • 错误的初始容量/负载因子设置:如果已知大致元素数量,提前 new HashMap(expectedSize) 可以避免扩容开销(或 ConcurrentHashMap 指定并发级别/容量)。
  • 过多的 GC pressure:大量短寿命对象(例如临时 wrapper)会触发频繁 GC。在高性能场景优先考虑对象复用、池化(小心内存泄漏)或 off-heap。
  • 没有基准测试就调优:使用 JMH 做微基准,且要在接近真实负载的环境下做对比(不同 GC、不同堆大小与 JIT 优化可能产生巨大差别)。

延伸阅读(源码 & 工具推荐)

  • 阅读 OpenJDK 中 HashMap, ConcurrentHashMap, ArrayList, ArrayDeque 的源代码(带注释)能极大提升理解。
  • JMH(Java Microbenchmark Harness)用于微基准测试。
  • fastutil / Eclipse Collections / Trove:primitive-specialized 集合库。
  • Caffeine:高性能缓存实现(推荐在生产中使用,胜过自造轮子)。

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。