Week 8 Day 1 — Threads and Shared Beans
Goal
Today I want the picture that makes every later concurrency rule obvious: one JVM, many stacks, shared singleton beans.
Main questions:
- What is a process vs a thread?
- What does a Spring Boot web app actually run?
- Why are singleton fields racy?
- What is thread-safe in this picture?
- What is a daemon thread?
1. Process vs thread
A process has its own address space. Two OS processes do not share a heap.
A thread shares the process heap, statics, and open files. It has its own stack and program counter (Week 1).
JVM process
├── Thread-http-1 stack ──┐
├── Thread-http-2 stack ──┼── heap: OrderService @1, OrderRepository @1
├── Thread-async-1 stack ──┘
└── GC threads
Locals live on the thread’s stack. Objects live on the heap. Two request threads calling the same @Service use two stacks and one object.
Memory sentence:
One JVM, many threads, shared singleton beans.
2. What Spring Boot runs
An embedded Tomcat/Jetty Boot app is one process. It is not “Tomcat in another process” unless I deployed a WAR that way.
Typical threads:
- request pool (Tomcat worker threads)
@Async/TaskExecutorpool (if I configured one)@Scheduledpool- GC and JVM internals
Two HTTP requests can run OrderService.place at the same time on the same bean. Stateless methods with only local variables and injected collaborators that are themselves thread-safe are fine. Mutable fields on the service are a race.
3. The singleton race
@Service
public class OrderService {
private int placed; // shared mutable field — a bug
public void place(CreateOrderRequest request) {
placed++; // not atomic, not visible
orders.save(...);
}
}
placed++ is read, add, write. Two threads can both read 0 and both write 1. Even volatile int does not make ++ atomic (Day 2).
Request data in a field is worse: user A’s DTO leaks into user B’s request (Week 1).
Safe patterns:
- No mutable fields on the singleton except injected
finalcollaborators - Locals and method arguments for request state
ConcurrentHashMap/AtomicInteger/ a real cache when I truly need shared mutable state- Immutable values (
record,String) are freely shareable
Memory sentence:
Request state stays on the stack (arguments and locals). The singleton holds collaborators, not the current user.
4. Thread-safe enough for a backend service
| Kind | Thread-safe to share? |
|---|---|
String, Long, records of values | Yes (immutable) |
ArrayList, HashMap | No, unless confined to one thread |
ConcurrentHashMap | Yes for its own operations |
JPA EntityManager | No — thread-bound (Day 5) |
Stateless @Service with final collaborators | Yes, if collaborators are |
“Thread-safe” means: concurrent calls do not corrupt state or publish stale writes. It does not mean “has synchronized on every method.” The cheapest thread-safety is no shared mutation.
5. Daemon threads
A JVM exits when non-daemon threads are done. GC threads are daemon. A new Thread I forget to mark, running a loop, can keep the process alive on shutdown — or a daemon I used for work can die mid-task when the main thread ends.
Spring’s executors are non-daemon by default for application work. On shutdown I want a graceful awaitTermination (Day 4), not a daemon that vanishes.
6. Spring connection
- Default bean scope is singleton: one instance, many request threads.
- Prototype beans are still not “one per thread” unless I use a scoped proxy.
@RequestScopebeans are per HTTP request, still not a substitute for understanding the singleton.
7. Common traps
Trap 1: “Tomcat is a separate process from Spring” in an embedded Boot app.
Trap 2: A counter or List field on a @Service “because there is only one service.”
Trap 3: Thinking locals are shared. Locals are per stack. Objects those locals point to may be shared.
Trap 4: static mutable state as a cache. That is global even across Spring contexts in tests.
Practice Questions and Answers
Question 1
Process vs thread? What does a Spring Boot app run?
Answer:
A process owns the address space. Threads share the heap and have their own stacks. A typical Boot web app is one JVM process with a pool of request threads plus extra pools. Singleton beans live on the heap and are called from many of those stacks at once.
Question 2
Why is a mutable field on @Service a bug?
Answer:
Every request thread uses the same instance. A mutable field is shared mutable state: races, lost updates, and leaked request data. Request data belongs in method arguments. Shared counters need atomics or a database.
Question 3
Are local variables thread-safe?
Answer:
The local itself is on one stack, so other threads cannot see that variable. If the local holds a reference to a shared object (the singleton, a map field), mutations of that object are still racy.
Question 4
What is the cheapest way to make a service thread-safe?
Answer:
Keep it stateless: private final collaborators, no request fields, immutable values. Add locks and concurrent collections only when I have real shared mutation.
Question 5
Is a prototype bean automatically thread-safe?
Answer:
No. Prototype means a new instance per injection (or lookup). Two threads can still share one prototype if I inject it into a singleton once. Scope is not a memory barrier.
Memory sentences
One JVM, many threads, shared singleton beans.
Request state stays on the stack. The singleton holds collaborators.
The cheapest thread-safety is no shared mutation.