Why Spring Beans Aren’t Thread‑Safe and How ThreadLocal Solves It
This article explains that Spring does not guarantee thread safety for its beans, describes the various bean scopes, clarifies why stateless singleton beans are safe, and shows how ThreadLocal works—including its implementation, usage, and potential memory‑leak pitfalls—so developers can write correct concurrent code.
Spring, as an IoC/DI container, manages many beans but does not ensure their thread safety; developers must write code to handle concurrency issues themselves.
Each bean has a scope that defines its lifecycle. Common scopes include:
singleton : the default scope; a single instance is created on first injection and lives as long as the Spring IoC container.
prototype : a new instance is created each time the bean is injected.
request: a single instance per HTTP request.
session : a single instance per HTTP session.
application : a single instance for the entire ServletContext.
websocket : a single instance for the lifetime of a WebSocket connection.
Most beans managed by Spring are stateless, making them effectively thread‑safe because they do not hold mutable state that could be corrupted by concurrent threads. Stateless objects include typical DO/DTO/VO models, as well as Service, DAO, and Controller classes that only operate on method‑local variables.
Even though a request scoped bean might seem to avoid cross‑request interference, Controllers are singleton by default, so multiple threads can still invoke them concurrently. Changing a Controller to prototype can avoid this but may degrade performance due to frequent object creation.
The root cause of thread‑safety problems lies in bean design. Avoid declaring mutable instance or class variables in beans; if shared mutable state is required, use ThreadLocal, synchronized, locks, or CAS mechanisms.
ThreadLocal
ThreadLocal provides a thread‑local variable, giving each thread its own copy of a value. It performs a shallow copy, so for reference types you should override initialValue() to provide a deep copy if needed.
ThreadLocal differs from lock‑based synchronization: locks coordinate multiple threads to share a variable, while ThreadLocal isolates the variable per thread, trading space for time.
Internally, ThreadLocal uses a nested class ThreadLocalMap, a linear‑probe hash map whose keys are the ThreadLocal objects (held via WeakReference) and whose values are the thread‑local copies.
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }
}
// ... other members ...
}Key fields in ThreadLocalMap include threadLocalHashCode, generated by nextHashCode() using a constant increment 0x61c88647 to achieve a uniform distribution.
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }Accessing a thread‑local value involves get(), which retrieves the current thread’s ThreadLocalMap. If the map is absent, setInitialValue() initializes it; otherwise the value is fetched from the map.
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
return setInitialValue();
}The set() and remove() methods also obtain the map via getMap() and then modify it.
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) map.set(this, value);
else createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) m.remove(this);
}The map is stored inside each Thread instance (fields threadLocals and inheritableThreadLocals), allowing fast lookup without a global synchronized map.
ThreadLocal Memory Leak
If a ThreadLocal is set to null and no strong references remain, the key becomes null in the map. Because the map lives in the thread, the associated value may remain reachable, causing a memory leak until the thread ends.
Using a weak reference for the key allows the ThreadLocal to be reclaimed; the map’s getEntry(), set(), and remove() methods clean up entries with null keys.
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key) return e;
if (k == null) expungeStaleEntry(i);
else i = nextIndex(i, len);
e = tab[i];
}
return null;
}However, these clean‑up actions only occur when set(), get(), or remove() are invoked. If a thread pool reuses threads without ever touching the ThreadLocal again, the stale entries can persist, leading to memory leaks. Therefore, it is essential to call remove() after using a ThreadLocal, especially in pooled‑thread environments.
References:
https://stackoverflow.com/questions/15745140/are-spring-objects-thread-safe
https://tarunsapra.wordpress.com/2011/08/21/spring-singleton-request-session-beans-and-thread-safety/
https://docs.spring.io/spring/docs/current/spring-framework-reference/index.html
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
