🧵 Concurrency Fundamentals · Advanced

Processes vs threads in Java

Threads share heap memory within a process; each has its own stack.

🧩 The mysteryYour phone runs dozens of apps that can't peek at each other's memory. Yet inside ONE Java program, a thousand threads can all scribble on the same object. What's the difference?

A process is a house

A process is a running program with its own private memory, like a house with locked doors. Two JVMs on one machine are two houses: a static field in one is invisible to the other. To share data between processes you need I/O: a network call, a database or a file.

Threads are roommates

Threads are independent paths of execution inside one process. Like roommates, they share the kitchen, the heap (objects and static fields), but each keeps a private notebook: its own stack of call frames and local variables. One JVM is one process, and it can run thousands of threads.

class Shop {
    static int visits;      // heap: shared
    void serve() {
        int tip = 5;        // stack: private
        var o = new Order(); // object: heap
    }
}
🤔 Think first

Private or shared?

A local variable, a static field, an object made with new, an instance field of a shared object. Which one is private to each thread?

Think about it, then reveal the answer

Only the local variable. It lives in a stack frame, and every thread has its own stack. The other three live on the heap, which every thread in the process can reach. (Careful: a local *reference* is private, but the object it points to is on the shared heap.)

🔮 Predict it

Your turn

The worker appends to a StringBuilder that main created. What does main print?

StringBuilder sb = new StringBuilder("hi");
Thread t = new Thread(() -> sb.append("!"));
t.start();
t.join();
System.out.println(sb);
  1. hi!
  2. hi
  3. Compile error
Show the answer

hi!. The StringBuilder object lives on the shared heap, so the worker changes the very object main sees. The lambda may capture sb because the *reference* is effectively final, while the object itself stays mutable. join() waits for the worker and guarantees main sees its writes.

⚠️ The trap

"static" is not machine-wide

A common mix-up: "static means shared by everyone." A static field is shared by every thread in one JVM. Service A setting Config.mode = "FAST" does nothing to Service B running in another JVM: B keeps its own value. Different processes, different heaps.

💼 In the real world

Why it matters

Chrome runs browser tabs as separate processes so one crashing tab can't corrupt the others. A Java web server does the opposite: every request runs on a thread inside one JVM, so all requests share caches cheaply. The catch? Sharing makes communication cheap and bugs easy. A mistake in shared state hits every request at once.

Key takeaways

  1. Processes are isolated; threads in one process share the heap
  2. Each thread has its own stack: local variables and call frames
  3. Sharing makes communication cheap — and bugs easy
  4. One JVM is one process; it can run thousands of threads

💡 A process is a house; threads are roommates who share the kitchen (heap) but each keep a private notebook (stack).

🤯 Did you know?

Each platform thread reserves its own stack, 1 MB by default on 64-bit Linux (tunable with -Xss). That cost is a big reason Java 21 added virtual threads.

Practice questions

Which of these is private to each thread?

  1. Local variables of a method call
  2. Static fields
  3. Objects created with new
  4. Instance fields of a shared object
Check your answer

Local variables of a method call. Local variables live in a stack frame, and every thread has its own stack. Static fields and objects live on the heap, which all threads share.

What does this print?

int[] box = {0};
Thread t = new Thread(() -> box[0] = 42);
t.start();
t.join();
System.out.println(box[0]);
  1. 42
  2. 0
  3. Compile error
  4. Throws InterruptedException
Check your answer

42. The array object lives on the shared heap, so the worker's write is visible to main. join() also guarantees main sees everything the worker did before it finished.

Next: you'll create your first thread, and see why calling run() instead of start() is one of Java's most common concurrency bugs.