在 Java 中实现一个内存栈,很多资料会提到 Stack 类和用 LinkedList 模拟栈,但从存储结构和运行时开销来看,这两者都不是最优解。ArrayDeque 基于循环数组,避免了链表节点的额外指针开销,也没有 Stack 那种全局同步锁,用它能写出一个更轻量、更快的内存栈。

为什么 Stack 和 LinkedList 不适合做高性能栈
Stack 是 Java 早期集合框架中的类,它继承自 Vector。Vector 内部对每个公开方法都使用了 synchronized 修饰,以保证线程安全。在单线程或者已经由外部控制并发的场景中,这种同步完全是多余的开销,会让每次 push 和 pop 都陷入锁竞争的准备状态,即使没有真正发生竞争,也会拖慢方法调用。
LinkedList 通过双向链表实现,每个元素被包装成 Node 对象,里面除了保存值,还要维护 prev 和 next 两个引用。大量入栈时会产生很多小对象,增加垃圾回收压力,而且链表节点在内存中分布不连续,CPU 缓存命中率低。对于只需要在同一端进出的栈来说,维护双向链接属于功能冗余。
ArrayDeque 的底层结构优势
ArrayDeque 使用一个普通的 Object 数组来存储元素,并通过两个 int 类型的头尾指针 head 和 tail 在数组中形成一个逻辑上的循环区间。当元素从头部加入或从尾部取出时,只需要移动指针并赋值数组下标,不需要创建节点对象,也不需要加锁。数组的连续内存布局对 CPU 缓存非常友好。
当数组装满时,ArrayDeque 会按比例扩容并拷贝数据到新数组,这种操作发生在容量临界处,平摊到每次操作上仍然是 O(1)。相比 LinkedList 持续产生新节点,ArrayDeque 在内存分配上更克制,长时间运行下 GC 更平稳。
用 ArrayDeque 封装一个内存栈
虽然 ArrayDeque 本身提供了 addFirst、pollFirst 等方法,但从语义上我们更希望暴露 push 和 pop。下面代码展示如何做一个简单的栈封装,并附带初始容量设置以减少早期扩容。
import java.util.ArrayDeque;
// 基于 ArrayDeque 的内存栈封装
public class MemoryStack<T> {
private final ArrayDeque<T> deque;
// 指定初始容量,降低扩容频率
public MemoryStack(int initialCapacity) {
deque = new ArrayDeque<>(initialCapacity);
}
// 入栈,放在队列头部
public void push(T item) {
deque.addFirst(item);
}
// 出栈,从队列头部取
public T pop() {
return deque.pollFirst();
}
// 查看栈顶但不移除
public T peek() {
return deque.peekFirst();
}
// 当前栈大小
public int size() {
return deque.size();
}
}
上面的代码把 ArrayDeque 的头部当作栈顶,addFirst 对应入栈,pollFirst 对应出栈。如果在多线程环境下使用,可以在外部加一把 ReentrantLock,而不是像 Stack 那样每个方法都内置锁,这样锁的粒度更可控。
如果担心扩容带来的短暂停顿,可以在创建栈时根据业务峰值预估容量。例如预期最多同时压入十万元素,就直接传入 131072 这类 2 的幂次初始值,ArrayDeque 会以该值作为数组长度起点。
三种实现方式对比
我们用一张表列出三者在关键维度上的差异,方便在做技术选型时快速判断。
| 实现方式 | 底层结构 | 线程安全 | 额外内存 | 单线程吞吐 |
|---|---|---|---|---|
| Stack | 数组加同步锁 | 内置锁 | 低 | 差 |
| LinkedList | 双向链表 | 不安全 | 高(节点对象) | 中 |
| ArrayDeque | 循环数组 | 不安全 | 低 | 优 |
从表中可以看出,ArrayDeque 在单线程吞吐和内存占用上都优于另外两者。如果你的系统对延迟敏感且栈操作非常频繁,比如做表达式解析、深度优先遍历或者临时对象池,换成 ArrayDeque 封装的栈往往能观察到明显的性能提升。
需要提醒的是,ArrayDeque 不允许插入 null 元素,而 Stack 和 LinkedList 可以。在把原有基于 LinkedList 的栈迁移过来时,要检查业务代码里是否依赖了 null 入栈,如果有,需要改成特殊标记对象或者提前做非空校验。
简单的压测示例
下面这段程序在同一个 JVM 里分别用三种结构做百万次入栈出栈,虽然具体耗时随机器变化,但相对关系通常稳定。
import java.util.ArrayDeque;
import java.util.LinkedList;
import java.util.Stack;
public class StackBenchmark {
static int COUNT = 1_000_000;
public static void main(String[] args) {
// Stack 测试
Stack<Integer> stack = new Stack<>();
long t1 = System.nanoTime();
for (int i = 0; i < COUNT; i++) stack.push(i);
while (!stack.isEmpty()) stack.pop();
System.out.println("Stack 耗时: " + (System.nanoTime() - t1));
// LinkedList 测试
LinkedList<Integer> list = new LinkedList<>();
long t2 = System.nanoTime();
for (int i = 0; i < COUNT; i++) list.push(i);
while (!list.isEmpty()) list.pop();
System.out.println("LinkedList 耗时: " + (System.nanoTime() - t2));
// ArrayDeque 测试
ArrayDeque<Integer> deque = new ArrayDeque<>(COUNT);
long t3 = System.nanoTime();
for (int i = 0; i < COUNT; i++) deque.addFirst(i);
while (!deque.isEmpty()) deque.pollFirst();
System.out.println("ArrayDeque 耗时: " + (System.nanoTime() - t3));
}
}
运行后一般会看到 ArrayDeque 的耗时明显小于 Stack,也经常优于 LinkedList,尤其在设置合理初始容量后,数组扩容次数减少,差距更明显。这个基准虽然简单,但足以说明在纯内存栈场景下,ArrayDeque 是更划算的底层选择。
总结来说,实现高性能内存栈并不需要自己写数组和指针,直接复用 ArrayDeque 并做一层语义封装即可。它规避了 Stack 的同步浪费和 LinkedList 的节点开销,在绝大多数单线程或外部加锁的场景里,都是比传统方案更优的替代品。
ArrayDequeStackLinkedList修改时间:2026-08-06 18:15:46