Back to Blog
Java

Java Stack: Usage, Methods & Concurrency

java stack: Understand Java's Stack class: LIFO behavior, methods, synchronization legacy, and why ArrayDeque is often the preferred replacement.

Java StackStack ClassDequeConcurrencyLIFO
A stylized stack data structure with LIFO operations and Java-related visual elements.

When a developer searches for java stack, the immediate expectation is a LIFO (last-in, first-out) data structure. Java provides a Stack class in java.util, but its design carries historical baggage that affects how it should be used today. The class extends Vector, which means it inherits all of Vector's methods and its synchronization model. That inheritance shapes performance characteristics, thread safety, and API design in ways that matter for real applications.

The Stack class was introduced in Java 1.0. It implements a classic stack with five methods: push, pop, peek, empty, and search. When you push an element, it goes onto the top; when you pop, you get the most recently pushed element and remove it; peek returns the top without removal. The search method returns the 1-based position from the top, or -1 if the element is not found. The underlying storage is an object array managed by Vector, so the stack grows automatically as elements are added.

Here is a minimal example that demonstrates the core operations:

import java.util.Stack; public class StackExample { public static void main(String[] args) { Stack<String> stack = new Stack<>(); stack.push("first"); stack.push("second"); stack.push("third"); System.out.println(stack.peek()); // third System.out.println(stack.pop()); // third System.out.println(stack.pop()); // second System.out.println(stack.empty()); // false System.out.println(stack.search("first")); // 1 (position from top) } }

The Synchronization Legacy of Vector

Because Stack extends Vector, every method in Stack is synchronized. This means each call to push, pop, peek, empty, and search acquires a lock on the stack object. In a single-threaded environment, that lock acquisition is unnecessary and adds overhead. In multi-threaded scenarios, it gives you thread safety for individual operations, but not for compound sequences.

Consider a typical stack pattern where you check whether the stack is empty and then pop. With Stack, each call is atomic, but the sequence if (!stack.empty()) stack.pop(); is not. Between the empty() check and the pop() call, another thread might pop the last element, causing EmptyStackException. To make that sequence safe, you still need external synchronization.

This legacy synchronization is the main reason many style guides and API documentation recommend using ArrayDeque instead. ArrayDeque is not thread-safe, but it implements the Deque interface and can be used with push and pop methods that match the stack semantics. When you need thread-safe stack behavior, you can wrap ArrayDeque with Collections.synchronizedDeque or use a ConcurrentLinkedDeque—both approaches give you more control over the concurrency policy.

ArrayDeque as a Replacement for Stack

The ArrayDeque class is a resizable-array implementation of the Deque interface. It is specifically designed for use as a stack or a queue and generally outperforms Stack in single-threaded code because it avoids synchronization. The LIFO methods are named identically: push, pop, and peek. This makes migration straightforward.

import java.util.ArrayDeque; import java.util.Deque; public class ArrayDequeStackExample { public static void main(String[] args) { Deque<String> stack = new ArrayDeque<>(); stack.push("first"); stack.push("second"); stack.push("third"); System.out.println(stack.peek()); // third System.out.println(stack.pop()); // third System.out.println(stack.pop()); // second System.out.println(stack.isEmpty()); // false } }

There is no search method in ArrayDeque. If you rely on search to find the 1-based depth of an element, you need to iterate manually or maintain a separate structure. In most modern Java code, that need is rare; the common stack operations are push, pop, and peek.

When you choose ArrayDeque, you lose the thread safety of Stack. That is typically acceptable because single-threaded stack usage is far more common, and forcing synchronization for every operation is a cost without benefit. If you truly need thread safety, you can lock externally or use a thread-safe deque implementation.

Common Mistakes When Using Stack

One frequent mistake is assuming that Stack and Deque are interchangeable in every context, especially regarding iteration order. Stack's iterator traverses from the bottom to the top, while ArrayDeque's iterator traverses from the first element (the head) to the last. For a stack, the head of the deque is actually the top of the stack. That reversal can confuse developers who iterate to debug stack contents.

Another mistake is using stack.search() to determine if an element exists and then using that position to index into the stack. Because search counts from the top (1-based), the returned value is not a list index. If you need a numeric index, you must subtract from the size, and even then, the relationship is fragile if the stack changes between calls.

Finally, relying on Stack being thread-safe without understanding the limits of that safety leads to subtle race conditions. For example, two threads calling pop() concurrently will not corrupt the internal array—each operation is atomic—but without coordination, you cannot guarantee which thread gets which element. That is usually not what developers mean when they ask for thread safety.

Concurrency Options for Stack-Like Behavior

When multiple threads need to push and pop from the same stack, the Stack class gives you individual operation atomicity. That may be sufficient for simple producer-consumer patterns, but it does not provide blocking behavior. A better choice for concurrent access is ConcurrentLinkedDeque, which is a thread-safe, lock-free deque. It implements the Deque interface and supports push, pop, and peek with atomic operations.

import java.util.concurrent.ConcurrentLinkedDeque; public class ConcurrentStack { private final ConcurrentLinkedDeque<String> stack = new ConcurrentLinkedDeque<>(); public void push(String item) { stack.push(item); } public String pop() { return stack.pollFirst(); // returns null if empty } public String peek() { return stack.peekFirst(); } }

Note that pollFirst() returns null when the deque is empty, rather than throwing EmptyStackException. This null-based signaling is often more convenient in concurrent code, but you must decide whether null is a valid element in your stack. If you cannot allow null elements, you might prefer to use removeFirst(), which throws NoSuchElementException when empty.

If you need blocking behavior—such as a producer thread waiting when the stack is full or a consumer waiting when it is empty—you should look at BlockingDeque implementations like LinkedBlockingDeque. These provide methods like putFirst, takeFirst, and offerFirst with timeouts, giving you fine-grained control over thread coordination.

Performance and Memory Characteristics

Stack's synchronization adds a lock acquisition and release for every method call. In single-threaded code, that overhead is measurable, especially in tight loops that push and pop many elements. ArrayDeque avoids that overhead and, because it is implemented as a resizable array, it uses contiguous memory, which improves cache locality. The amortized cost of push and pop is O(1), though resizing when the internal array fills triggers a copy of the existing elements.

Memory-wise, both Stack and ArrayDeque hold references to objects, so the stack itself uses a small amount of memory per slot. The remaining memory footprint depends on the objects stored. Stack inherits Vector's capacity-management methods, like ensureCapacity() and trimToSize(), which you can use to pre-allocate or shrink the internal array. ArrayDeque also allows initial capacity specification, but it does not expose a public trim method.

For stack-like operations, the practical difference in memory is typically negligible unless you are storing millions of elements. In that case, choose a data structure that minimizes resizing—such as starting ArrayDeque with an appropriate capacity—and avoid the unnecessary synchronization of Stack.

When to Use Stack vs ArrayDeque

The decision comes down to how much you value the historical API and what your concurrency requirements are. For new code, ArrayDeque is the recommended choice for a stack in a single-threaded context. It has a cleaner API, no unnecessary synchronization, and predictable performance. You lose the search method, but that method is rarely essential.

If you are working with legacy code that already uses Stack, migrating to ArrayDeque is usually straightforward. Change the type declaration from Stack to Deque and replace new Stack<>() with new ArrayDeque<>(). The push, pop, and peek calls remain the same, but you must update any usage of empty() to isEmpty() and handle search() differently.

There is no scenario where Stack is clearly better than ArrayDeque for new development. Even when you need thread safety, you are better off using ConcurrentLinkedDeque or LinkedBlockingDeque, because those implementations are designed for concurrent access and offer more explicit control over behavior. The only reason to keep Stack is when you are maintaining code that cannot change, and even then, understanding the synchronization model helps you write safer code.

The Cost of Synchronization in a Hot Path

To illustrate why synchronization matters, consider an algorithm that pushes and pops millions of elements, such as an iterative tree traversal or a backtracking solver. In a single-threaded run, the synchronized Stack will perform more work per operation than an unsynchronized ArrayDeque. The difference is direct—the lock is acquired and released even when there is no contention. The JVM may optimize uncontended locks, but that optimization is not guaranteed and it adds complexity to the runtime.

For a concrete feel, you can run a microbenchmark with System.nanoTime() on your own environment, but the results will vary by JDK version and hardware. The important point is that the synchronization is an extra instruction that has a cost, and it buys you nothing in a single-threaded context.

If you are writing a library that exposes a stack to callers, you might be tempted to use Stack to guarantee thread safety. That guarantee is incomplete for compound operations, as discussed earlier. A better approach is to offer an unsynchronized Deque and let the caller wrap it or choose a concurrent implementation. That gives the caller the choice between safety and performance.

Alternative Stack-Like Implementations

Java also provides java.util.LinkedList, which implements both List and Deque, and can be used as a stack. Its performance is comparable to ArrayDeque for push and pop, but it has the overhead of node objects and poor cache locality. For most stack use cases, ArrayDeque is preferred over LinkedList.

Another alternative is to implement a custom stack backed by your own array. This gives you full control over capacity policy and null handling, but it is rarely worth the effort unless you need serialization or special behavior that the standard libraries do not provide. The standard ArrayDeque covers the vast majority of stack requirements.

Final Section: Compounding Operations and Atomicity

In concurrent use, the fundamental limitation of Stack is that compound sequences are not atomic. A classic example is a token bucket algorithm where you pop a token only if one is available. With Stack, you would write:

synchronized (stack) { if (!stack.isEmpty()) { return stack.pop(); } }

This external synchronization is necessary because the check and the pop must be seen as a single operation by other threads. The built-in synchronization of each method does not help here. In contrast, ConcurrentLinkedDeque has no compound atomic operations either, but because its methods are lock-free, you might think you are safe. You are not—you still need to use pollFirst() in a loop or use compute-style methods if they exist. For a simpler atomicity pattern, LinkedBlockingDeque offers blocking pollFirst(long timeout, TimeUnit unit) which atomically waits and pops, making it easier to implement a blocking stack consumer.

The takeaway is that when you see Stack in a codebase, you should evaluate both the performance cost of synchronization and the completeness of the thread-safety guarantee. In many cases, replacing it with ArrayDeque or a concurrent deque improves clarity and performance without any loss of functionality. When you do need atomic compound operations, plan to introduce your own locking, because no stack implementation provides that out of the box.

java stack: Practical Usage and Code Examples | RYUSLOG DEV