emirhan
← Back to writing

Writing

Why Singleton Does Not Mean Thread-Safe

Why a singleton Spring Bean can still have race conditions, and how stateless design or atomic operations can make concurrent access safer.

3 min read
  • #Spring
  • #Java
  • #Concurrency

Hello everyone, I wanted to write a short post about something I learned recently: the relationship between singleton and thread safety.

At first, these two concepts may look connected. If there is only one instance of an object, we may think that it should be safe. But actually, singleton and thread safety answer different questions.

Singleton is about how many object instances exist. Thread safety is about whether the system works correctly when multiple threads access the same object at the same time.

Let’s look at a simple Spring service:

@Service
public class CounterService {    
    private int count = 0;

    public int increment() {
        return ++count;
    }
}

By default, Spring Beans are created with Singleton Scope. This means Spring keeps one instance of this Bean inside the ApplicationContext.

However, having only one instance does not mean that only one request can use this service at a time. A backend application can process multiple HTTP requests at the same time, and these requests can run on different threads.

So, multiple threads can access the same CounterService instance.

The real problem here is shared mutable state.

The count field belongs to the service instance and can be shared by all threads. Also, this value can change.

Let’s look at this line:

count++;

This looks like one simple operation, but it actually includes a few steps: reading the current value, increasing it by one, and writing the new value back.

For example, imagine count is 2.

Thread A reads the value 2. Before Thread A writes the new value, Thread B may also read 2. Then both threads increase the value and write 3.

Since increment() was called twice, we expect the result to be 4, but the result can be 3.

This is a simple example of a race condition.

So, being singleton does not mean being single-threaded. There can be only one object, but many threads can use that object at the same time.

This is why Spring services are usually designed to be stateless.

For example:

@Service
public class CalculatorService {

    public int add(int a, int b) {
        return a + b;
    }
}

This service does not keep any request-specific data inside a field. Everything it needs comes from method parameters. Because of this, calls from different threads do not affect each other.

If we really need shared state, then we need to manage concurrent access correctly. In the counter example, we can use AtomicInteger:

@Service
public class CounterService {    
    private final AtomicInteger count = new AtomicInteger();

    public int increment() {
        return count.incrementAndGet();
    }
}

Here, incrementAndGet() provides an atomic operation. However, AtomicInteger does not automatically make every stateful service thread-safe. In more complex cases, different synchronization methods may be needed.

The main thing I learned from this topic is actually very simple:

Singleton is about object lifecycle. Thread safety is about concurrent access.

A Spring Bean can be singleton and still not be thread-safe.

So instead of only asking, “Is this Bean singleton?”, I think a better question is:

“Does this object contain shared mutable state that can be accessed by multiple threads?”

In many backend applications, removing this state as much as possible is simpler and safer than trying to add synchronization later.

Thanks for reading. I hope this small difference was useful for you too.