bitset线程不安全,因内部long[]数组更新无同步,多线程并发set或flip会导致数据错乱。

BitSet 本身不是并发包(java.util.concurrent)中的类,它属于 java.util 包,且默认线程不安全。直接在高并发场景下用原生 BitSet 做位操作(比如多线程同时 set(index) 或 flip(index))会导致数据错乱——因为其内部 long[] 数组的更新未加同步,底层位运算(如 `words[wordIndex] |= (1L
为什么不能直接把 BitSet 当作并发位图用
原生 BitSet 的核心问题在于:单个 long 元素的位更新不可分。例如两个线程同时对同一个 word(long 单元)中不同 bit 位置执行 set 操作:
- 线程 A 想设置 bit 3 → 读取 word=0x00,计算 0x00 | 0x08 = 0x08,CAS 写回
- 线程 B 同时想设置 bit 7 → 也读取 word=0x00,计算 0x00 | 0x80 = 0x80,CAS 写回
- 最终只保留其中一个结果(0x08 或 0x80),另一个被覆盖
这和 AtomicLong 面临的问题本质相同:所有线程争抢同一内存地址,导致 CAS 高频失败、重试、伪共享加剧。
LongAdder 思想如何迁移到并发位图
LongAdder 的核心是“分段 + 局部竞争 + 最终聚合”,这一思想可被借鉴来构建线程安全的高性能位图,关键改造点有三个:
- 分段存储:不再用单一 long[],而是将位空间按固定粒度(如每 64 位为一段)划分为多个 Cell 数组,每个 Cell 封装一个 volatile long,并用 @Contended 隔离缓存行
- 线程局部映射:通过 ThreadLocalRandom probe 值哈希到某个 Cell 上操作对应 bit;若发生竞争(CAS 失败),则尝试扩容 Cell 数组或跳转到下一个 Cell,避免全局阻塞
- 延迟聚合:set()、flip() 等写操作只更新局部 Cell;size() 或 containsAll() 等读操作才遍历所有 Cell + base 求和或逻辑或,不强求实时一致性,换得吞吐提升
实际可行的替代方案:RoaringBitmap + 并发包装
现实中,直接手写“并发 BitSet”既复杂又易错。更务实的做法是组合成熟组件:
- 用 RoaringBitmap 替代 BitSet:它天然按区间(container)组织数据,每个 container(如 bitmap container)可单独加锁或用 AtomicReferenceFieldUpdater 控制更新,粒度远细于整个数组
- 对高频写场景,采用 分片 + 写缓冲:例如按用户 ID hash 到 64 个 RoaringBitmap 实例,每实例配独立读写锁;或引入无锁环形缓冲区暂存待写入 bit,由单个消费者线程批量 flush 到位图
- 若只需“存在性判断+去重计数”,优先考虑 BloomFilter(ConcurrentBloomFilter) 或 LongAdder + 哈希分桶:比如 UV 统计中,用 MurmurHash3 把 ID 映射到 1024 个 LongAdder 桶,每个桶计数该 hash 段内出现次数,最后 sum 所有桶 —— 不保证精确去重,但内存极省、吞吐极高
小结:思想比实现更重要
LongAdder 没有提供并发 BitSet,但它揭示了一种通用优化范式:当原子变量成为瓶颈时,就用空间换时间,把“单点竞争”拆成“多点局部竞争”。这个思路在位图场景下同样成立——重点不是让每个 bit 都原子,而是让每次写操作尽可能落在互不干扰的内存区域。真正上线时,应优先评估业务对精度、延迟、内存的容忍度,再选择 RoaringBitmap 分片、布隆过滤器、或 LongAdder 分桶等合适路径。










