Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Learn › Spring › IoC container and DI

Singleton beans and state

My order shows someone else's name

By default, only one instance of a Spring bean is created, and all requests share it. If you store a value that differs per request in a field of that bean, another request that comes in at the same time overwrites the value.

Checked against real execution · Checked by running code on JDK 21

Concepts in this lab

Stateless singleton beans
A singleton bean is one instance that all request threads use together. So if you store data that differs per request in an instance variable, the data of different requests gets mixed up. A singleton bean must be designed to be stateless: it keeps no per-request state in fields and handles such data only through the local variables and parameters of its methods.
Bean scope
This is the range over which a bean instance is created and kept. The default, singleton, creates only one instance per container, and everyone shares it. prototype creates a new instance each time it is requested. In a web environment there are also the request scope, which creates one per HTTP request, and the session scope, which creates one per session.
ThreadLocal
This is a variable that lets each thread store its own independent value. The same variable has a different value in each thread, so thread-specific information can be kept without synchronization. Spring keeps the current transaction and the authentication information this way. In a thread pool, threads are reused, so you must always clear the value with remove() after use.
Thread safety
This is the property of working correctly even when several threads use something at the same time. Code is thread-safe if there is no shared state, if the state is immutable, or if access is properly synchronized. A Spring singleton bean is used by many request threads together, so it must be made thread-safe (usually by keeping no state).
Race condition
This is a situation in which two or more threads read and write the same data at the same time, and the result depends on the order of execution. count++ consists of three steps: read the value, add 1, and write it back. If the steps of two threads are interleaved, one of the increments is lost. It typically stays hidden in tests and appears under load.

Try these

Running with the default settings

  1. There is only one OrderService bean in the container. Thread 1 handles Alice's request and thread 2 handles Bob's request, and they call order() on the same object at almost the same time.
  2. Thread 1 (Alice): It stores "Alice" in the field currentUser.
  3. Thread 2 (Bob): It stores "Bob" in the same field. There is only one field, so "Alice" is overwritten.
  4. Thread 1 (Alice): After it finishes the stock check, it reads the field again and creates the order. The field contains "Bob". Alice's order is created under Bob's name.
  5. Thread 2 (Bob): It reads the field and creates the order. The value is "Bob".

Result of this run

Code

@Service
class OrderService {                 // there is only one instance
    private String currentUser;      // a field that all requests share

    public Order order(String user, Item item) {
        this.currentUser = user;     // (1) store
        checkStock(item);            // some other work for a moment
        return new Order(currentUser, item);   // (2) read it back
    }
}

Assumptions of this model

The model assumes the order in which the two requests overlap in the worst way (both store the value before either reads it). In reality, the problem shows up only when requests overlap, so it is hard to reproduce. That the default scope of a Spring bean is singleton and that a ThreadLocal keeps a separate value for each thread follow the official Spring and Java documentation. This was checked by actually running code on JDK 21 on this PC. When two threads wrote a value to a field of one object, both orders got the name that was written later, and with local variables the values did not get mixed up. In a pool with a single thread, when the ThreadLocal was not cleared, the next task read the previous value, and when remove was called, the result was null.

Previous lab: Bean creation and injection · Next lab: Bean scopes

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these