生产者消费者模型的关键不是启动两个线程,而是定义队列容量和过载行为。无界队列会把短期流量峰值变成内存增长;有界 队列则让系统在过载时等待、拒绝或降级。
Table of contents
Open Table of contents
运行有界队列实验
下载背压样例下载, 运行:
java -cp target/classes dev.elaine.concurrency.BackpressureSample
输出为:
第一个事件=true
队列已满时第二个事件=false
消费=event-1
第二次 offer 在 50 毫秒内得不到容量后返回 false。调用方能据此返回忙碌、记录丢弃、写入备用存储或让上游降速。
四组 API 表达不同失败策略
| 行为 | 立即失败 | 阻塞等待 | 超时等待 |
|---|---|---|---|
| 插入 | add 抛异常、offer 返回 false | put | offer(e, timeout, unit) |
| 取出 | remove 抛异常、poll 返回 null | take | poll(timeout, unit) |
服务请求线程通常优先带超时版本,避免无限等待。后台批处理可以使用 put/take,但仍要响应中断,让关闭流程可终止。
容量来自等待预算
容量不是越大越安全。粗略估算可以从 Little’s Law 开始:系统中的平均任务数约等于到达率乘平均停留时间。例如消费者 每秒处理 100 个任务,允许排队 2 秒,队列起点约为 200;随后用峰值、尾延迟和对象大小压测修正。
队列指标至少包含当前深度、容量使用率、等待时间、拒绝数、任务年龄和消费耗时。只监控线程池活跃数看不见陈旧任务。
公平性与吞吐
ArrayBlockingQueue 可选择公平锁,让等待较久的线程优先,但通常牺牲吞吐。LinkedBlockingQueue 若不传容量,默认上限
非常大,实际效果接近无界;在线服务应显式设置容量。
单机队列不提供进程崩溃后的持久性。任务不能丢失时使用数据库 Outbox 或消息系统,并同时设计幂等消费和重试上限。
下一步
阅读ThreadPoolExecutor 参数、拒绝与动态调节,把队列背压扩展到完整 任务执行器。