Java 队列怎么用:ArrayDeque 入队、出队与失败重试
- 发布时间:2026-10-26 07:00:11
- 本文热度:浏览 4 赞 0 评论 0
- 文章标签: Java ArrayDeque 队列
- 全文共1字,阅读约需1分钟
待处理任务按 A、B、C 的顺序进入队列。A 第一次失败后,要马上再试,还是先让 B、C 执行?这不是队列替调用者决定的策略:把 A 放回队首,它会再次抢在其他任务前面;放回队尾,才会先处理已经等待的 B、C。
Java 的 ArrayDeque 足够完成这种由单个线程拥有的内存任务队列。它同时支持队首和队尾操作,因此先选定方向:从队尾加入,从队首取出。下面把这个约定写进一个完整程序,再处理空队列、失败重试和业务容量上限。
把处理顺序直接打印出来
使用 JDK 17,保存为 TaskQueueDemo.java。这里用集合指定“第一次失败”和“每次失败”的任务,模拟处理结果,没有发送网络请求。每个任务最多尝试两次;每次失败都会增加已尝试次数,达到上限就记入失败清单,不再入队。
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
public class TaskQueueDemo {
record Task(String id, int attempts) {
Task {
if (id == null || id.isBlank() || attempts < 0) {
throw new IllegalArgumentException("invalid task");
}
}
}
record Result(List<String> done, List<String> failed, List<String> trace) {}
static Result process(List<String> ids, Set<String> failFirst,
Set<String> alwaysFail, int maxAttempts) {
Objects.requireNonNull(ids);
Objects.requireNonNull(failFirst);
Objects.requireNonNull(alwaysFail);
if (maxAttempts < 1 || maxAttempts > 10) {
throw new IllegalArgumentException("maxAttempts must be 1..10");
}
Deque<Task> pending = new ArrayDeque<>();
Set<String> distinct = new HashSet<>();
for (String id : ids) {
Task task = new Task(id, 0);
if (!distinct.add(id)) throw new IllegalArgumentException("duplicate task id");
pending.addLast(task);
}
List<String> done = new ArrayList<>();
List<String> failed = new ArrayList<>();
List<String> trace = new ArrayList<>();
while (!pending.isEmpty()) {
Task task = pending.removeFirst();
int attempt = task.attempts() + 1;
boolean rejected = alwaysFail.contains(task.id())
|| (failFirst.contains(task.id()) && attempt == 1);
trace.add(task.id() + "#" + attempt + (rejected ? ":FAIL" : ":OK"));
if (!rejected) {
done.add(task.id());
} else if (attempt < maxAttempts) {
pending.addLast(new Task(task.id(), attempt));
} else {
failed.add(task.id());
}
}
return new Result(List.copyOf(done), List.copyOf(failed), List.copyOf(trace));
}
static boolean enqueueIfRoom(Deque<String> queue, String id, int limit) {
if (limit < 1 || id == null || id.isBlank()) {
throw new IllegalArgumentException("invalid admission rule");
}
if (queue.size() >= limit) return false;
queue.addLast(id);
return true;
}
static void equal(Object expected, Object actual) {
if (!Objects.equals(expected, actual)) {
throw new AssertionError("expected=" + expected + " actual=" + actual);
}
}
static void fails(Class<? extends RuntimeException> type, Runnable action) {
try { action.run(); }
catch (RuntimeException error) {
if (type.isInstance(error)) return;
throw error;
}
throw new AssertionError("expected " + type.getSimpleName());
}
public static void main(String[] args) {
Result result = process(List.of("A", "B", "C"), Set.of("A"), Set.of(), 2);
equal(List.of("B", "C", "A"), result.done());
equal(List.of(), result.failed());
equal(List.of("A#1:FAIL", "B#1:OK", "C#1:OK", "A#2:OK"), result.trace());
System.out.println("TRACE=" + result.trace());
System.out.println("DONE=" + result.done());
Result exhausted = process(List.of("D"), Set.of(), Set.of("D"), 2);
equal(List.of(), exhausted.done());
equal(List.of("D"), exhausted.failed());
equal(List.of("D#1:FAIL", "D#2:FAIL"), exhausted.trace());
System.out.println("EXHAUSTED=" + exhausted.trace());
Deque<String> empty = new ArrayDeque<>();
equal(null, empty.peekFirst());
equal(null, empty.pollFirst());
fails(NoSuchElementException.class, empty::removeFirst);
fails(NullPointerException.class, () -> empty.offerLast(null));
System.out.println("EMPTY=poll:null,remove:NoSuchElementException");
Deque<String> hinted = new ArrayDeque<>(2);
equal(true, hinted.offerLast("A"));
equal(true, hinted.offerLast("B"));
equal(true, hinted.offerLast("C"));
equal(3, hinted.size());
System.out.println("INITIAL_CAPACITY_2_SIZE=" + hinted.size());
Deque<String> admitted = new ArrayDeque<>();
equal(true, enqueueIfRoom(admitted, "A", 2));
equal(true, enqueueIfRoom(admitted, "B", 2));
equal(false, enqueueIfRoom(admitted, "C", 2));
equal(List.of("A", "B"), new ArrayList<>(admitted));
System.out.println("ADMITTED=" + admitted);
Deque<String> stack = new ArrayDeque<>();
stack.push("A");
stack.push("B");
equal("B", stack.pop());
equal("A", stack.pop());
fails(IllegalArgumentException.class,
() -> process(List.of("A", "A"), Set.of(), Set.of(), 2));
fails(IllegalArgumentException.class,
() -> process(List.of("A"), Set.of(), Set.of(), 0));
System.out.println("STACK_POP=[B, A]");
System.out.println("ALL_CHECKS_PASSED");
}
}
在文件所在目录运行:
javac -encoding UTF-8 TaskQueueDemo.java
java TaskQueueDemo
输出为:
TRACE=[A#1:FAIL, B#1:OK, C#1:OK, A#2:OK]
DONE=[B, C, A]
EXHAUSTED=[D#1:FAIL, D#2:FAIL]
EMPTY=poll:null,remove:NoSuchElementException
INITIAL_CAPACITY_2_SIZE=3
ADMITTED=[A, B]
STACK_POP=[B, A]
ALL_CHECKS_PASSED
A 第一次执行后,队列变成 [B, C, A]。因此完成顺序与最初入队顺序不同,但每次从队首取任务的规则始终没有改变。D 则在第二次失败后退出队列,程序能走到最终输出,而不是一直重试。
TRACE 有四条记录,但完成的是三个不同任务。统计执行尝试次数时可以看 trace,统计成功任务数则看 done,重试不应该被当作又完成一份新工作。对固定的 n 个初始任务,每个最多尝试 m 次,本程序最多取出 n × m 次;没有新任务不断加入,也没有失败后把次数清零,因此所有任务都失败时仍能结束。空的输入列表则直接得到三份空结果。
不要把两套端点操作混着用
Deque 表示双端队列,常见操作可以先按业务动作记忆:
- 正常入队用
addLast或offerLast,取下一项用removeFirst或pollFirst。 - 只看下一项,用
getFirst或peekFirst,它们不会把元素移走。 - 当栈使用时,
push和pop都在队首工作,最后压入的 B 会先弹出。
add、offer、remove、poll 这些不带方向的方法也能使用;对 Deque 来说,普通队列形式对应尾部加入、头部移除。完整程序选择带 First、Last 的名称,是为了让重试分支的方向一眼可见。
如果把失败重试改成 addFirst,A 将紧接着再次执行,B、C 继续等待。这并非 API 错误,而是另一种调度策略。持续把失败项塞到队首时,其他任务可能长时间排不上,所以本例选择尾部重试,并给总尝试次数设上限。代码里的 attempts 表示已经执行过几次,不是还剩几次;第一次失败后保存 1,下次取出才算第二次。
初始输入还单独检查了重复任务编号。这是本次清单的输入规则,不是 ArrayDeque 会自动去重;换成允许相同任务编号的业务,应重新决定如何区分两个任务。失败项重新入队沿用原编号和已尝试次数,不会重新经过初始清单的去重步骤。把“重复提交”和“同一任务重试”混用一个判断,很容易让重试直接消失。
空队列是正常状态,还是调用顺序错误
pollFirst 在空队列返回 null,removeFirst 则抛出 NoSuchElementException。查看元素也有同样的成对形式:peekFirst 返回 null,getFirst 抛异常。区别不在取出的元素,而在没有元素时如何表达结果。
轮询下一项时,“当前没有任务”通常是正常状态,poll 比捕获异常直接。完整程序先在同一个线程判断 isEmpty,再 removeFirst,队列不会被其他线程在两步之间抢空,因此使用抛异常的形式也成立。
ArrayDeque 不允许 null 元素,这使 poll 返回的 null 可以明确表示空队列。如果任务本身没有内容,应在入队之前拒绝,而不是用 null 作为一个待执行任务塞进去。程序中的空白编号也由 Task 的构造规则拒绝,与容器禁止 null 是两层不同的检查。
构造时的 2,并不是只能放两个任务
new ArrayDeque<>(2) 指定的是初始容量需求,不是队列的业务上限。示例连续 offer 三次都成功,size 为 3。ArrayDeque 会按需要扩容;不能用 offerLast 返回 true 就推断自己实现了“最多两个任务”。
真正的上限在 enqueueIfRoom 里:先判断当前长度,满了直接返回 false,未满才添加。调用者必须处理这个 false,例如让用户稍后重试,而不是继续报告已受理。这里的“满”是应用制定的规则,不是 ArrayDeque 自带的固定容量。
每次重试都创建新的 Task,编号与次数保存在不可变的 record 里;完成后返回的三个列表也通过 List.copyOf 固定下来。这样调用者拿到 Result 后不能继续追加一项,改掉本次处理结果。待处理的 ArrayDeque 本身仍是可变容器,程序只让 process 内部持有它。
这段判断和添加只适用于本例的单线程拥有方式。多线程共享时,两次调用可能同时看到尚未满,随后都加入;ArrayDeque 本身也没有并发访问保护。需要线程间等待、唤醒和有界容量时,应选择相应的 BlockingQueue 实现并按其并发合同使用,不能给这个方法随意加几行判断就声称具备相同能力。
这份程序只保证有限输入在一次进程运行中按指定规则处理。任务从队列取出到记录完成之间,如果进程退出,内存状态不会自动恢复;“失败后重新入队”也没有提供跨进程确认或持久化。它适合清楚展示本地调度顺序,不能直接代替需要可靠交付的任务系统。