1. 栈与队列的基础认知
第一次接触数据结构时,导师在黑板上画了两个图形:一个垂直的筒子(栈)和一条水平的管道(队列)。这个场景我记了十几年,因为这两种结构实在太经典了。栈就像我们叠盘子,最后放上去的总是最先被拿走(LIFO);而队列则像排队买奶茶,先来的顾客先得到服务(FIFO)。在Java中实现它们,不仅是语法练习,更是理解计算机如何处理数据流的绝佳入口。
实际开发中,栈的身影无处不在:方法调用时的调用栈、IDE的撤销操作、浏览器的前进后退;队列则在任务调度、消息传递等场景大显身手。虽然Java集合框架已经提供了成熟的实现(java.util.Stack和java.util.Queue),但亲手实现一次,你会对源码设计有更深的体会。去年优化一个交易系统时,正是对队列实现的深入理解,帮我定位到了线程阻塞的症结所在。
2. 栈的Java实现剖析
2.1 基于数组的定容栈
定容栈的实现就像给储物柜分配固定格子。我们先定义存储数据的数组和栈顶指针:
java复制public class FixedCapacityStack<T> {
private T[] items;
private int top; // 栈顶指针(下一个空位索引)
@SuppressWarnings("unchecked")
public FixedCapacityStack(int capacity) {
items = (T[]) new Object[capacity]; // Java不允许直接创建泛型数组
top = 0;
}
}
关键操作的核心逻辑:
- push():检查溢出后存入数据并移动指针
- pop():检查下溢后返回数据并移动指针
- peek():仅查看不弹出
java复制public void push(T item) {
if (top == items.length) {
throw new RuntimeException("Stack overflow");
}
items[top++] = item;
}
public T pop() {
if (isEmpty()) {
throw new RuntimeException("Stack underflow");
}
return items[--top]; // 注意前置递减
}
实际工程中更推荐使用动态扩容策略。当数组填满时,创建一个更大的数组并复制元素,类似ArrayList的实现机制。
2.2 基于链表的动态栈
链式栈就像用绳子串起的珠子,每个节点记住下一个节点的位置:
java复制private static class Node<T> {
T data;
Node<T> next;
Node(T data) {
this.data = data;
}
}
public class LinkedStack<T> {
private Node<T> top;
public void push(T item) {
Node<T> newNode = new Node<>(item);
newNode.next = top;
top = newNode;
}
public T pop() {
if (top == null) throw new NoSuchElementException();
T item = top.data;
top = top.next;
return item;
}
}
链表实现的优势在于:
- 无需预先分配固定空间
- 插入删除操作都是O(1)时间复杂度
- 不会出现数组实现的"假溢出"问题
但每个元素需要额外内存存储指针,且CPU缓存命中率不如数组实现。在LeetCode解题时,链表栈更常用;而在高性能交易系统中,经过优化的数组栈往往是首选。
3. 队列的Java实现方案
3.1 环形数组队列
数组实现队列的难点在于处理"假溢出"——数组前端有空位但尾部已满。环形缓冲区是经典解决方案:
java复制public class CircularQueue<T> {
private T[] items;
private int head; // 队首索引
private int tail; // 队尾索引(下一个插入位置)
private int count; // 元素计数
public CircularQueue(int capacity) {
items = (T[]) new Object[capacity];
}
public void enqueue(T item) {
if (count == items.length) {
throw new RuntimeException("Queue overflow");
}
items[tail] = item;
tail = (tail + 1) % items.length; // 环形计算
count++;
}
public T dequeue() {
if (count == 0) throw new NoSuchElementException();
T item = items[head];
items[head] = null; // 防止内存泄漏
head = (head + 1) % items.length;
count--;
return item;
}
}
模运算(%)是环形处理的关键。注意当存储对象时,出队后要将数组位置置null,避免持有过期引用。我在金融系统开发中,曾因忽略这点导致内存泄漏,最终通过HeapDump才定位到问题。
3.2 双栈实现队列
这是面试常考的经典问题:用两个栈模拟队列操作。思路如下:
java复制public class TwoStackQueue<T> {
private Stack<T> inStack = new Stack<>();
private Stack<T> outStack = new Stack<>();
public void enqueue(T item) {
inStack.push(item);
}
public T dequeue() {
if (outStack.isEmpty()) {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
if (outStack.isEmpty()) throw new NoSuchElementException();
return outStack.pop();
}
}
虽然每个元素可能经历两次入栈出栈,但摊还分析显示每个操作的时间复杂度仍是O(1)。这种实现方式在函数式编程中很常见,因为栈的不可变特性更容易实现线程安全。
4. 工程实践中的进阶考量
4.1 线程安全实现
基础的栈/队列实现不是线程安全的。以生产者-消费者模式为例,我们需要保证:
- 写操作互斥
- 队列空时消费者等待
- 队列满时生产者等待
使用ReentrantLock和Condition的完整实现:
java复制public class BlockingQueue<T> {
private final T[] items;
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
// 其他字段同环形队列
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[tail] = item;
tail = (tail + 1) % items.length;
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
T item = items[head];
items[head] = null;
head = (head + 1) % items.length;
count--;
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
这种模式在Kafka等消息中间件的底层实现中广泛使用。注意要用while而不是if检查条件,防止虚假唤醒(spurious wakeup)。
4.2 性能优化技巧
-
伪共享(false sharing)处理:在多核环境下,对head/tail指针使用@Contended注解或填充缓存行
java复制// JDK8+ @sun.misc.Contended private volatile int head; -
无锁队列:对于超高并发场景,可考虑AtomicReferenceArray+CAS实现
java复制public class LockFreeQueue<T> { private final AtomicReferenceArray<T> items; private final AtomicInteger head = new AtomicInteger(0); private final AtomicInteger tail = new AtomicInteger(0); public boolean offer(T item) { int t = tail.get(); if (t - head.get() >= items.length()) return false; items.set(t % items.length(), item); tail.compareAndSet(t, t + 1); return true; } } -
批量操作:在日志收集等场景,可实现批量入队方法减少锁竞争
java复制public int batchEnqueue(List<T> items) { lock.lock(); try { int canAccept = Math.min(items.size(), items.length - count); for (int i = 0; i < canAccept; i++) { this.items[tail] = items.get(i); tail = (tail + 1) % this.items.length; } count += canAccept; notEmpty.signalAll(); return canAccept; } finally { lock.unlock(); } }
5. 常见问题诊断手册
5.1 栈溢出诊断
现象:调用栈过深导致StackOverflowError
- 递归调用没有终止条件
- 循环依赖的对象序列化
- 解决方案:改用循环结构或增加终止条件检查
5.2 队列阻塞排查
现象:生产者消费者死锁
- 消费者崩溃后未发送通知
- 队列满时生产者等待但无消费者释放
- 解决方案:设置超时机制,添加监控线程
5.3 性能瓶颈分析
案例:日志系统吞吐量下降
- 同步队列竞争激烈
- 使用JProfiler定位锁竞争热点
- 最终方案:改用Disruptor环形队列
5.4 内存泄漏定位
场景:长时间运行后OOM
- 出队操作未清空数组引用
- 使用MAT分析堆转储文件
- 发现队列中残留百万级过期对象
在分布式系统中,我曾遇到一个队列积压问题:由于消费者处理速度慢,内存队列不断增长最终导致OOM。解决方案是加入背压机制,当队列长度超过阈值时,拒绝新的生产请求并返回429状态码。