原创

Java 队列怎么用:ArrayDeque 入队、出队与失败重试

待处理任务按 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 实现并按其并发合同使用,不能给这个方法随意加几行判断就声称具备相同能力。

这份程序只保证有限输入在一次进程运行中按指定规则处理。任务从队列取出到记录完成之间,如果进程退出,内存状态不会自动恢复;“失败后重新入队”也没有提供跨进程确认或持久化。它适合清楚展示本地调度顺序,不能直接代替需要可靠交付的任务系统。

正文到此结束
评论插件初始化中...
Loading...