Skip to main content

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:

  1. What is a process vs a thread?
  2. What does a Spring Boot web app actually run?
  3. Why are singleton fields racy?
  4. What is thread-safe in this picture?
  5. 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 / TaskExecutor pool (if I configured one)
  • @Scheduled pool
  • 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 final collaborators
  • 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

KindThread-safe to share?
String, Long, records of valuesYes (immutable)
ArrayList, HashMapNo, unless confined to one thread
ConcurrentHashMapYes for its own operations
JPA EntityManagerNo — thread-bound (Day 5)
Stateless @Service with final collaboratorsYes, 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.
  • @RequestScope beans 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.

Next: Week 8 Day 2 — synchronized, Lock, and volatile