← Back to list

Writing Thread-Safe Code in Java (Before It Breaks in Production)

Here’s a certain kind of bug that doesn’t show up in development. Your code works locally, it passes every test, and it even clears QA…

Basuki Nath · 2026-06-14 20:04 · 0 claps · 4.0 min read
#java #threading-in-java #multithreading #thread-safety #java-interview-questions
Open on Medium ↗
Wiki topics: SAF · Safety & Alignment 📚 · Books & Reading

Writing Thread-Safe Code in Java (Before It Breaks in Production)

Here’s a certain kind of bug that doesn’t show up in development. Your code works locally, it passes every test, and it even clears QA without issues. Then one day, under real traffic, something quietly starts going wrong.

A counter skips numbers. A cache returns stale data. A request fails without any clear reason. There are no errors, no stack traces, just incorrect behavior. If you’ve ever chased one of these issues, you already know how this story usually ends. In most cases, it turns out to be a concurrency problem.

Early in my career, I worked on a service that maintained a simple in-memory cache backed by a HashMap. It was straightforward and worked perfectly under normal conditions. But once traffic picked up, we started noticing inconsistencies. Values would sometimes disappear or revert, not consistently, but often enough to become a concern.

The root cause wasn’t obvious at first. Eventually, we realized multiple threads were reading and writing to the same map without any coordination. Nothing crashed, nothing failed loudly, but the state became unreliable. That experience makes something very clear: concurrency bugs don’t break your application, they break your trust in it.

At its core, most thread-safety problems come from the same issue: multiple threads interacting with shared mutable state.

Take a simple example:

class Counter {
    int count = 0;

    void increment() {
        count++;
    }
}

This looks harmless, but count++ is not a single operation. It involves reading the value, incrementing it, and writing it back. If two threads interleave these steps, one update can overwrite the other. You end up with incorrect data and no indication that anything went wrong.

The natural instinct is to start thinking about locks, but the more important first step is to question the design itself. Do you really need shared mutable state here?

One of the most effective ways to write thread-safe code is to avoid the problem entirely. Immutability is a powerful tool for this. If an object cannot change after it is created, threads can safely use it without coordination.

final class User {
    private final String name;

    User(String name) {
        this.name = name;
    }
}

There are no race conditions here because there is nothing to race over. In practice, systems that lean more toward immutability and stateless design tend to have far fewer concurrency issues. This is not just about clean code, it is about reducing operational risk.

Of course, not everything can be immutable. At some point, you will have shared state that changes. That is where synchronization comes into play.

synchronized void increment() {
    count++;
}

This guarantees correctness by allowing only one thread to execute the critical section at a time. It is simple and reliable, but it introduces contention. Under low load, this may not matter. Under high load, it can easily become a bottleneck.

This leads to a common pattern in production systems where a bug is fixed using synchronization, but a performance problem is introduced at the same time. The code becomes correct, but it doesn’t scale.

As systems evolve, especially around hot paths, locking often becomes too expensive. This is where atomic classes provide a better alternative.

AtomicInteger count = new AtomicInteger();
count.incrementAndGet();

Instead of blocking threads, atomic operations rely on a mechanism called compare-and-swap. The idea is simple: update the value only if it hasn’t been changed by another thread. If it has, retry the operation. This allows multiple threads to make progress without waiting on each other, which leads to better performance under contention.

Another construct that often gets misunderstood is volatile.

volatile boolean running = true;

A volatile variable ensures visibility, meaning changes made by one thread are immediately visible to others. It also prevents certain kinds of instruction reordering. However, it does not make compound operations safe. Using it for something like incrementing a counter still leads to race conditions.

In practice, volatile works well for flags and state indicators, but not for operations that involve multiple steps.

Collections are another area where concurrency issues show up frequently. Classes like HashMap and ArrayList are not thread-safe, and using them across threads without coordination eventually leads to inconsistent behavior.

Modern Java provides concurrent alternatives designed for these scenarios.

Map<String, String> map = new ConcurrentHashMap<>();

Unlike a regular HashMap, this does not lock the entire structure. It allows multiple threads to operate on different parts simultaneously, which makes it far more scalable. In many real-world systems, switching to concurrent collections is one of the simplest and most effective fixes.

As you start looking across these approaches, a pattern begins to emerge. Thread safety is not just about choosing the right tool. It is about reducing the situations where threads have to coordinate at all.

That is why many modern systems prefer stateless services and asynchronous workflows. Instead of multiple threads competing to modify shared data, work is structured so that each step operates independently.

CompletableFuture.supplyAsync(() -> fetch())
                 .thenApply(data -> transform(data))
                 .thenAccept(this::save);

In this model, the need for locks is minimized because there is very little shared mutable state. The system becomes easier to reason about and scales more naturally.

If there is one consistent lesson across production systems, it is this: most concurrency problems are not caused by threads themselves, but by how we design shared state. The more state you share, the more coordination you need. The more coordination you need, the harder your system becomes to scale and maintain.

Thread safety is not something you add after bugs appear. It is something you account for when designing the system.

Key Takeaways

  • Most concurrency issues originate from shared mutable state.
  • Immutability significantly reduces the need for synchronization.
  • synchronized ensures correctness but can limit scalability.
  • Atomic classes provide efficient, non-blocking alternatives.
  • volatile guarantees visibility, not atomicity.
  • Concurrent collections are essential for shared data structures.
  • Good system design minimizes the need for thread coordination.

The tricky part about concurrency is not that it is complex, but that it is subtle. Problems rarely appear early, and when they do, they rarely fail loudly. That is what makes them dangerous.

The systems that scale well are not the ones that manage concurrency perfectly, but the ones that avoid unnecessary contention in the first place.


메타데이터
post_id
e1eeb2ea6b85
slug
writing-thread-safe-code-in-java-before-it-breaks-in-production-e1eeb2ea6b85
url
https://medium.com/@basukinath/writing-thread-safe-code-in-java-before-it-breaks-in-production-e1eeb2ea6b85
canonical_url
https://medium.com/@basukinath/writing-thread-safe-code-in-java-before-it-breaks-in-production-e1eeb2ea6b85
author_url
https://medium.com/@basukinath
status
ok
fetched_at
2026-08-11 02:01:24