投票系统应选用ConcurrentHashMap存储票数、ConcurrentHashMap.newKeySet()记录已投ID、动态锁对象保障原子性、流式排序生成实时有序视图。

投票系统核心数据结构怎么选
用 Map 存候选人名和票数最直接,但要注意键值重复和线程安全。如果只是单线程控制台程序,HashMap 足够;若涉及多线程模拟(比如多个用户并发投票),必须换成 ConcurrentHashMap,否则会出现计数丢失。
候选人名单建议额外用 List 或 Set 管理,避免仅靠 Map 的 key 集合做校验——因为 Map 可能还没初始化某个候选人,直接 get() 会返回 null,容易引发 NullPointerException。
如何防止重复投票
光靠“输入姓名”无法识别同一人,必须引入唯一标识。最简方案是要求用户输入学号/工号(字符串),并用另一个 Set 记录已投票 ID:
private static SetvotedIds = ConcurrentHashMap.newKeySet(); // 投票前检查 if (votedIds.contains(userId)) { System.out.println("该用户已投过票"); return; } votedIds.add(userId);
注意:ConcurrentHashMap.newKeySet() 是 Java 8+ 提供的线程安全集合;若用 HashSet 配 synchronized,代码更冗长且易漏锁。
立即学习“Java免费学习笔记(深入)”;
- 不推荐用 IP 或时间戳做去重——本地测试全是
127.0.0.1,时间戳精度不够 - 控制台程序中,“用户ID”由人工输入,需在提示语里明确要求(如“请输入学号:”),否则逻辑再严也挡不住乱输
投票操作的原子性怎么保障
一次投票包含两个动作:检查是否已投 + 增加票数。这两步必须原子执行,否则并发时可能 A、B 同时通过检查,然后都给同一候选人 +1,实际只应 +1 次。
解决方式不是加全局锁(性能差),而是对具体候选人加细粒度锁:
private static final Mapvotes = new ConcurrentHashMap<>(); private static final Map candidateLocks = new ConcurrentHashMap<>(); public static void vote(String candidateName, String userId) { if (!isValidCandidate(candidateName)) return; if (votedIds.contains(userId)) return; // 获取该候选人的专属锁对象 Object lock = candidateLocks.computeIfAbsent(candidateName, k -> new Object()); synchronized (lock) { // 再次确认(防止锁外已变更) if (votedIds.add(userId)) { votes.merge(candidateName, 1, Integer::sum); } } }
这里用 computeIfAbsent 动态生成锁对象,避免为不存在的候选人预分配锁;merge 是线程安全的计数更新方式,比 put(k, get(k)+1) 更可靠。
结果查询为什么不能直接遍历 Map
调用 votes.entrySet() 遍历没问题,但若想按票数排序输出,别写 new ArrayList(votes.entrySet()).sort(...) —— 这会创建中间列表,且排序后仍是无序 Map,下次遍历又乱序。
正确做法是每次查询时生成有序视图:
List> sorted = votes.entrySet().stream() .sorted(Map.Entry. comparingByValue().reversed()) .collect(Collectors.toList());
注意点:
-
reversed()必须显式调用,否则默认升序(得票少的排前面) - 不要把
sorted结果缓存成字段——Map 内容随时可能变,缓存会导致结果过期 - 如果候选人很多(>1000),流式排序比手动循环 + 数组快,但不用过度优化,业务系统里查票不是高频操作
真实场景中,没人会手敲一百行投票逻辑——但理解这四点,才能看懂 Spring Boot + MyBatis 版本里 DAO 层的 @Select 和 Service 层的 synchronized 块到底在防什么。










