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
- Default settings — Stored in a field of the bean. When two requests overlap, the values get mixed up.
- In a local variable — Each request has its own variable on its own stack, so the values do not get mixed up.
- ThreadLocal, not cleaned up — Concurrent requests are safe, but the previous value remains when the thread is reused.
- ThreadLocal, cleaned up — It is safe if you clear the value when the request ends.
Running with the default settings
- 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.
- Thread 1 (Alice): It stores "Alice" in the field currentUser.
- Thread 2 (Bob): It stores "Bob" in the same field. There is only one field, so "Alice" is overwritten.
- 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.
- Thread 2 (Bob): It reads the field and creates the order. The value is "Bob".
Result of this run
- All requests share the fields of a singleton bean. If you keep a value that differs per request in a field, concurrent requests overwrite each other's values. This never shows up when you test alone, and it appears only intermittently when many users come in at once.
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