最轻量线程安全消息队列用LinkedList+wait()/notify()实现,需while循环等待、add后notify、消息体预留type/priority/expireAt等字段,文件持久化用Files.write或PrintWriter,启动用CountDownLatch控制顺序。

用 wait() + notify() 实现线程安全的消息队列
Java里最轻量、不依赖第三方库的消息提醒系统,核心是让生产者(发消息)和消费者(处理提醒)解耦。直接用 LinkedList 配合 synchronized 块 + wait()/notify() 是最可控的做法——比裸用 BlockingQueue 更利于理解阻塞逻辑,也比自己加 Thread.sleep() 轮询更省资源。
常见错误是忘了在 while (queue.isEmpty()) 里等待,写成 if 导致虚假唤醒后直接 poll() 出 null;还有忘记在 addMessage() 末尾调用 notify(),导致消费者永远卡住。
-
getMessage()方法必须用while判断空队列,不能用if - 每次
addMessage()后必须notify(),哪怕队列没满也要唤醒等待中的消费者 - 不要在
wait()外层加try-catch InterruptedException后吞掉异常——应向上抛或重新设置中断状态
消息体设计要预留扩展字段,别只存 content 和 receiver
实际项目中,“提醒”不是只有“发给谁+说什么”。比如你要支持邮件、短信、站内信混合推送,就得在 Message 类里提前留好 type("email"/"sms")、priority(int)、expireAt(long 时间戳),甚至 retryCount。否则后期加字段要改所有生产者代码,还容易漏初始化。
一个典型坑是把接收者写成 String receiver,结果业务要求支持群发(多个邮箱)、分级通知(先发组长再发总监)。不如一开始定义为 List,哪怕初期只加一个元素。
立即学习“Java免费学习笔记(深入)”;
- 加
timestamp字段,方便后续做延迟提醒或去重判断 - 避免用
public成员变量,getter/setter 虽啰嗦,但未来加校验逻辑(如邮箱格式检查)不用动调用方 - 如果消息可能持久化到文件,字段类型优先选
String、long、int,少用Date或自定义对象,减少序列化麻烦
文件落盘做兜底时,别用 FileWriter 直接追加
消息提醒系统一旦挂掉,内存里的队列就丢了。加文件持久化是刚需,但很多人用 FileWriter("queue.log", true) 每次写一条,结果高并发下日志错乱、换行丢失、甚至覆盖前一条——因为 FileWriter 不保证原子写入,也没缓冲区控制。
正确做法是:用 Files.write() 配合 StandardOpenOption.APPEND,且每次写入带完整换行符;或者更稳一点,用 PrintWriter 包一层,调用 println() 并显式 flush()。
- 写入前对
Message做 JSON 序列化(比如用Gson.toJson()),别拼字符串,避免内容含换行或引号导致解析失败 - 文件名建议带日期,如
queue_20260204.log,方便按天归档和排查 - 启动时先读文件重建队列,但要注意重复加载风险——可加个
processedId文件记录最后成功消费的 ID
多线程启动顺序错了,系统会“假死”
主函数里如果先 start() 消费者线程,再 start() 生产者,大概率消费者一上来就 wait() 等消息,而生产者还没开始放数据,整个流程卡住。这不是 bug,是线程调度的正常现象。
解决方法很简单:生产者线程启动后,主线程 sleep(100) 再启动消费者;或者更干净的做法——用 CountDownLatch 控制就绪顺序:生产者初始化完 countDown(),消费者 await() 等它就位。
- 别依赖
Thread.join()来等生产者“发完”,它根本不知道啥时候算“发完” - 消费者线程的
while(true)循环里,catch(InterruptedException)后建议直接break,避免被中断后还傻等 - 如果后续要支持优雅停机,得给消费者加个
volatile boolean running = true,循环条件改成while(running)
真正难的从来不是写出让消息“动起来”的代码,而是想清楚:哪条消息绝对不能丢?哪类提醒必须 5 秒内触达?文件写一半进程崩溃了怎么办?这些边界问题不提前想,上线后查日志都找不到断点。










