Stack 类在多线程下变慢是因为它继承 Vector,所有 public 方法均加 synchronized,导致单线程也需走同步入口,多线程时锁竞争严重;且扩容机制与栈使用模式不匹配,易引发内存抖动。

为什么 Stack 类在多线程下会莫名其妙变慢
因为 Stack 直接继承 Vector,而 Vector 的每个 public 方法(比如 push()、pop()、peek())都加了 synchronized。哪怕你只在一个单线程里用,JVM 仍要走同步入口;真到多线程场景,所有操作串行排队,锁竞争直接拉满。
常见错误现象:Stack 在高并发压测中吞吐量骤降,CPU 利用率低但线程大量阻塞在 WAITING 状态——看堆栈基本都是卡在 Vector 的同步块里。
- 不是“用了线程才出问题”,而是“只要用了
Stack,就默认背上了不必要的同步开销” - 即使你手动加了外部锁,
Stack内部又套一层,纯属冗余 -
Vector的扩容机制(翻倍)和Stack的使用模式(频繁小幅度增减)也不匹配,容易引发不必要内存抖动
替代 Stack 的三个实际选择及适用场景
Java 6 起官方文档就明确建议用 ArrayDeque 替代 Stack;它不是线程安全的,但正因如此,单线程性能提升明显,且 API 更贴近栈语义。
- 单线程高频操作(如解析表达式、DFS 遍历)→ 用
ArrayDeque:无锁、数组实现、push()/pop()均为 O(1) - 需要线程安全 + 栈行为 → 不要用
Stack,改用Collections.synchronizedCollection(new ArrayDeque())或更细粒度控制(比如用ConcurrentLinkedDeque,但注意它不保证 LIFO 严格顺序) - 必须用 legacy API 兼容老代码 → 至少把
Stack声明类型改为Deque,避免暴露Vector的非栈方法(如elementAt()、removeAllElements())
示例对比:
立即学习“Java免费学习笔记(深入)”;
Stack<String> old = new Stack<>(); // 继承 Vector,全方法 synchronized<br>Deque<String> now = new ArrayDeque<>(); // 推荐,无锁,接口更纯粹
Stack 的 API 设计缺陷:为什么 peek() 和 empty() 这类方法让人困惑
Stack 混入了太多 Vector 的遗留方法,比如 elementAt(int)、setSize(int)、copyInto(Object[]),它们和“栈”概念完全无关,却公开暴露,容易误用。
-
empty()返回boolean,但名字像 void 方法;新人常写成stack.empty();当作清空操作 -
peek()在空栈时抛EmptyStackException,而现代集合(如Deque.peek())返回null,更易判空处理 -
search(Object)是线性扫描,时间复杂度 O(n),但名字没提示性能代价,容易在循环里误用
这些设计让 Stack 既不像集合工具类,也不像专注栈语义的类型,边界模糊,维护成本高。
从字节码和 JDK 源码看继承 Vector 的真实代价
反编译 Stack.push() 就能看到:它只是调用了 super.addElement(),而 Vector.addElement() 开头就是 synchronized (this)。没有抽象、没有优化、没有绕过——完完全全透传了 Vector 的锁粒度和扩容逻辑。
- JDK 21 中
Vector已被标记为@Deprecated(forRemoval = true),Stack自然也被波及 -
ArrayDeque底层是循环数组,push()实际调用addFirst(),没有同步、没有装箱/拆箱冗余(泛型擦除后仍是数组操作) - 如果你正在迁移旧项目,注意
Stack的toString()输出格式(方括号+逗号分隔)和ArrayDeque不同,可能影响日志或调试断言
真正麻烦的不是替换那几行代码,而是那些隐含依赖 Vector 行为的子类或反射调用——比如有人重写了 Stack.size() 却忘了同步逻辑,结果破坏了父类契约。







