原子类无法解决本地抽奖去重,因其仅保障单变量内存操作的原子性,无法约束文件I/O的并发访问,而抽奖去重需“查文件→判断→写文件”整体逻辑的线程/进程安全,属典型的Check-Then-Act竞态,文件流本身无跨线程互斥能力。
直接用并发原子类配合文件流做本地抽奖去重,这条路走不通,也不推荐。
为什么原子类解决不了本地抽奖去重
Java 的 AtomicInteger、AtomicLong 等原子类只保证单个变量的读-改-写操作线程安全,但它们无法控制文件 I/O 的并发访问。抽奖去重的核心是“判断某用户是否已中奖 + 写入新中奖记录”这一整套逻辑,而文件流(如 FileWriter、RandomAccessFile)本身不具备跨进程/跨线程的互斥能力:
- 多个 JVM 进程同时打开同一文件,会相互覆盖或抛出 IOException
- 即使单进程多线程,FileWriter 默认不加锁,不同线程写同一文件极易导致内容错乱
- 原子类只能保护一个数字(比如抽奖次数),但无法保证“查文件→没中过→写文件”这三步不被其他线程插队——这是典型的 Check-Then-Act 竞态
真正可行的本地去重方案
若坚持纯本地、无数据库、轻量级,应绕开“文件流直写”,改用以下更稳妥的方式:
-
内存+原子容器+定期落盘:用
ConcurrentHashMap<String, Boolean>存已中奖用户 ID(key 去重天然),用AtomicInteger计总数;所有抽奖逻辑在内存完成;程序退出或定时(如每 100 次)将 map 序列化为 JSON 文件备份。重启时加载该文件恢复状态 -
带文件锁的临时文件写入:每次写中奖记录前,用
FileChannel.lock()获取独占锁,写完立即释放。注意锁是 JVM 级别,不能跨进程,且 Windows 下对 NFS 不友好 -
SQLite 嵌入式数据库:比文件流更可靠,支持 ACID 和唯一索引。建表
CREATE TABLE winners (user_id TEXT UNIQUE),插入失败即说明重复。零配置、单 jar 可运行,远胜手写文件逻辑
如果非要操作文件,必须加外部协调机制
仅靠 Java 原生类无法安全实现。可行底线做法:
- 用
java.nio.channels.FileLock包裹整个抽奖流程(含读取已有名单 + 判断 + 追加写入) - 所有操作封装在 try-with-resources 中,确保锁释放
- 文件格式建议用 CSV 或一行一 JSON,避免解析冲突
- 务必设置超时(如 lock(0, Long.MAX_VALUE, true) 配合中断处理),防死锁
本质上,抽奖去重不是并发计数问题,而是状态一致性问题。原子类适合计数器,不适合持久化去重。选对工具比硬套概念更重要。

















