Avoid Hidden Thread‑Safety Bugs in Spring Boot 3: 6 Common Pitfalls and Fixes

This article examines six typical concurrency mistakes in Spring Boot 3—misusing volatile, unsafe ConcurrentHashMap patterns, exposing mutable collections, removing synchronized, abusing parallel streams, and invisible shutdown flags—showing why they occur and providing concrete, thread‑safe code replacements.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Avoid Hidden Thread‑Safety Bugs in Spring Boot 3: 6 Common Pitfalls and Fixes

1. Introduction

In multithreaded Java development, developers often introduce subtle thread‑safety bugs because they misunderstand the guarantees of the Java concurrency primitives. The article lists six typical pitfalls and demonstrates how to eliminate them with proper use of Atomic classes, explicit locks, immutable wrappers, and correct visibility controls.

2. Practical Cases

2.1 Assuming volatile guarantees atomicity

private volatile boolean initialized = false;
if (!initialized) {
    loadCache();
    initialized = true;
}

Because volatile only ensures visibility, multiple threads can evaluate !initialized as true simultaneously, causing loadCache() to run more than once. The fix is to use an atomic compare‑and‑set operation.

Fix

private final AtomicBoolean initialized = new AtomicBoolean(false);
if (initialized.compareAndSet(false, true)) {
    loadCache();
}

2.2 Misusing ConcurrentHashMap for compound logic

Map<String, Session> sessions = new ConcurrentHashMap<>();
if (!sessions.containsKey(id)) {
    sessions.put(id, createSession());
}

Two threads may both see containsKey(id) as false, each create a session, and one overwrite the other, leading to random logout behavior.

Fix

sessions.computeIfAbsent(id, k -> createSession());
computeIfAbsent

performs the check‑and‑put atomically, guaranteeing that only one session is created.

2.3 Exposing a mutable collection

List<Rule> rules = new ArrayList<>();
public List<Rule> getRules() {
    return rules;
}

The returned reference allows callers to modify the internal list, breaking encapsulation and thread safety.

Fix

public List<Rule> getRules() {
    return Collections.unmodifiableList(rules);
}
// or
public List<Rule> getRules() {
    return List.copyOf(rules);
}

Both alternatives return an immutable view, preventing external mutation.

2.4 Removing synchronized for performance

public synchronized void updateBalance(int amount) {
    balance += amount;
}

When synchronized is removed, balance += amount becomes a non‑atomic read‑modify‑write sequence, allowing race conditions and lost updates.

Fix

private final AtomicInteger balance = new AtomicInteger();
balance.addAndGet(amount);
AtomicInteger

implements the operation with CAS, offering higher throughput under contention than a heavyweight lock.

2.5 Parallel streams can degrade I/O‑bound workloads

transactions.parallelStream().forEach(this::process);

Using the global ForkJoinPool for CPU‑intensive tasks causes thread contention when many services invoke parallelStream(). If the stream body performs blocking I/O (database, network, file), the pool’s threads become idle, leading to thread starvation, high CPU usage, and increased latency.

Fix

ExecutorService executor = Executors.newFixedThreadPool(8);
transactions.forEach(tx -> executor.submit(() -> process(tx)));

Explicitly sized thread pools separate I/O work from CPU work and avoid exhausting the common pool.

2.6 Updating a stop flag without visibility guarantees

boolean shutdown = false;
while (!shutdown) {
    doWork();
}

Thread A may cache shutdown in a CPU register, while Thread B sets it to true. Without a happens‑before relationship, the change is not guaranteed to become visible, so the loop may never exit.

Fix

volatile boolean shutdown = false;
while (!shutdown) {
    doWork();
}

Marking the flag as volatile establishes the required memory‑visibility guarantees. Alternatively, use the interrupt mechanism:

while (!Thread.currentThread().isInterrupted()) {
    try {
        doWork();
    } catch (InterruptedException e) {
        break;
    }
}

A full example demonstrates creating a worker thread, interrupting it from the main thread, and exiting cleanly.

Conclusion

Thread‑safe collections alone do not make composite operations safe; developers must combine them with atomic primitives, immutable exposure, and proper visibility controls. Applying the fixes above eliminates the six common concurrency bugs and yields more reliable Spring Boot services.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencySpring BootThread SafetyConcurrentHashMapatomic
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.