ConcurrentLinkedQueue 的无锁队列,适合什么场景
「ConcurrentLinkedQueue 的无锁队列,适合什么场景」
去年在优化一个相机预览模块的帧缓冲区时,我把原来的 LinkedBlockingQueue 换成了 ConcurrentLinkedQueue。当时的想法很直接:无锁队列,没有锁竞争,多线程环境下性能应该更好。结果上线后,用户反馈的是手机发烫,而不是变快了。抓了一段 systrace 一看,CPU 时间没有明显减少,GC 的频率反而翻了一倍。问题就出在这个"无锁"上。
帧缓冲池里的 GC 抖动
那个模块负责管理相机预览的 SurfaceImage 对象池。业务逻辑是后台线程从相机拿帧,处理后抛给主线程渲染。为了复用对象,我们维护了一个 ConcurrentLinkedQueue<ImageWrapper> 作为缓冲池。代码大概长这样:
ConcurrentLinkedQueue<ImageWrapper> pool = new ConcurrentLinkedQueue<>();
// 释放回池
pool.offer(wrapper);
// 取用
ImageWrapper item = pool.poll();单看 API 设计,offer 和 poll 都是无锁的,多线程并发应该很高效。但在 Android Studio Profiler 的 Memory 视图里,这个池子每秒钟产生数百个 Allocation,而且绝大部分不是 ImageWrapper 本身,而是 ConcurrentLinkedQueue 的内部节点 Node。在 Android 13(API 33)的 ART 环境下,每个被回收的 Node 对象都要走完整的 GC 路径。当相机帧率达到 30fps,池子流转频繁时,这些临时 Node 对象成了 GC 的主要压力来源。
ConcurrentLinkedQueue 的链表结构决定了每次 offer 都要 new 一个 Node,poll