AtomicInteger 能替代 synchronized 实现单选项原子计数,但不支持取消投票、条件更新等复杂逻辑;ConcurrentHashMap.putIfAbsent 更适合动态选项存储,需规范 key 归一化处理,且查总数与各选项值无法强一致。

AtomicInteger 能不能直接替代 synchronized 投票计数
能,但只适用于单个选项的原子增减。它解决的是 ++ 这种非原子操作的竞态问题,比如多个线程同时执行 voteCount++ 会导致漏统计。
常见错误现象:AtomicInteger.get() 返回值比预期少,尤其在高并发压测时;你以为用了原子类就万事大吉,结果发现投票总数对不上。
- 必须用
incrementAndGet()或addAndGet(1),别写count.get() + 1; count.set(...)—— 这样就退化成普通 int 了 - 如果要支持「取消投票」或「改投」,得额外处理逻辑,
AtomicInteger本身不记录谁投的、投过几次 - 它不提供「读-改-写」的条件更新(比如“只有当前是 0 才设为 1”),这种得用
compareAndSet()
ConcurrentHashMap.putIfAbsent 为什么比 synchronized(map) 更适合存选项
因为投票系统通常要动态支持新增选项(比如用户发起新提案),而 ConcurrentHashMap 允许多线程安全地初始化键值对,且不会锁整个 map。
使用场景:每次收到一个新选项名,先确保它在统计 map 中存在,再对其计数器做原子操作。
立即学习“Java免费学习笔记(深入)”;
- 别用
map.get(key) == null ? map.put(key, new AtomicInteger()) : ...—— 这中间有竞态窗口,两个线程可能同时创建新AtomicInteger - 正确做法是
map.putIfAbsent(key, new AtomicInteger()),返回的是已存在的或刚插入的实例,再调用它的incrementAndGet() - 注意
putIfAbsent在 JDK 8+ 才有高效实现;JDK 7 及以前建议用computeIfAbsent(JDK 8+)或手动 double-check
Map 的 key 设计容易踩哪些坑
key 看似简单,但实际中常因大小写、空格、前后缀导致重复统计或漏匹配。
常见错误现象:用户投 “苹果” 和 “苹果 ”(带空格)被算作两个选项;或 “Apple” 和 “apple” 分开计数。
- 入库前必须 normalize:统一 trim()、toLowerCase(),必要时加正则清理(如去掉多余符号)
- 避免用原始用户输入直接当 key,尤其是中文里全角/半角空格、换行符都可能混入
- 如果选项来自数据库或配置,注意编码一致性;HTTP 请求参数默认是 UTF-8,但某些老客户端可能发 GBK 编码字节流,服务端没 decode 就当 key 会出乱码 key
并发下「查总数 + 查各选项」为什么不能靠两次遍历保证一致性
因为 ConcurrentHashMap 的迭代器是弱一致的 —— 遍历时其他线程可以修改,你拿到的「总和」和「各选项值」可能来自不同快照。
性能影响:想强一致就得加锁或复制全量,但代价是吞吐下降;多数投票场景不需要绝对实时一致,但前端显示不能出现「各选项加起来不等于总数」这种明显 Bug。
- 不要写:先
map.values().stream().mapToInt(AtomicInteger::get).sum(),再单独返回 map —— 这两个动作之间数据可能已变 - 如果必须强一致(比如审计场景),用
mappingCount()不行,得用new HashMap(map)快照,但注意内存和 GC 压力 - 更轻量的做法是:所有更新走同一把细粒度锁(比如按选项 hash 分段),或者接受最终一致性,加注释说明「统计有秒级延迟」
真正麻烦的不是怎么写原子操作,而是怎么定义「一次投票」—— 是以请求为准?还是以落库为准?中间如果重试、超时、幂等校验失败,计数器和业务状态就容易脱钩。这个边界不厘清,再多的 AtomicInteger 也救不了。










