The app that froze on “Download”
Riya's college-canteen app downloads the menu, today's offers and the dish photos one after another when it opens. On slow campus Wi-Fi the screen stays blank for six seconds, and the Back button doesn't respond.
Sir, each download is only two seconds. Why does the whole app freeze?
Because one thread is doing everything: download, download, download, and only then draw the screen. While it waits for the network, nothing else can happen.
So I need more threads?
Yes. Your program is one process, and inside it you can run several threads at once. They share the same data, but each one follows its own path through the code. Let's see what that really means inside the JVM.
What are processes and threads?
A process is a program in execution, with its own memory space and system resources, isolated from other processes by the operating system. A thread is the smallest unit of execution that the OS can schedule; it runs inside a process. All threads of a process share its heap memory, loaded classes and open files, but each thread has its own stack (method frames and local variables) and its own program counter. In Java, every program starts with one thread called main, and more threads are represented by java.lang.Thread objects.
A process is a restaurant; threads are the cooks in its kitchen. They share the same fridge and stove (the heap), but each cook follows their own recipe card (their own stack).
Words you'll meet in this lesson
main().Process vs thread
| Process | Thread | |
|---|---|---|
| Memory | Its own, isolated | Shares the process's heap |
| Stack | Each of its threads has one | Its own stack |
| Cost to create | High (new memory space) | Low (a stack and some bookkeeping) |
| Talking to others | Needs files, sockets, pipes | Just read and write shared objects |
| If it crashes | Other processes are safe | An uncaught error ends only that thread, but bad shared data can hurt the others |
| Java example | Each java Main you run | main, plus every Thread you start |
Exam tip: “Threads share the heap and method area (classes, static fields); each thread has its own stack and program counter.” This one sentence answers most process-vs-thread questions. Also remember: the JVM exits when all non-daemon threads have finished.
The threads you'll meet
The main thread
Created by the JVM to run main(). Everything you've written so far ran on it.
Your threads
Created with new Thread(…) and started with start() (Lesson 12.2).
Daemon threads
Background helpers (autosave, cleanup). The JVM doesn't wait for them when it exits.
setDaemon(true)JVM's own threads
Garbage collector and JIT compiler threads run alongside yours, unseen (Module 8).
systemMeet the main thread
public class Main {
public static void main(String[] args) {
Thread me = Thread.currentThread();
System.out.println("main runs on: " + me.getName());
Thread helper = new Thread(() -> {});
System.out.println(helper.getName() + ", state " + helper.getState()
+ ", daemon " + helper.isDaemon() + ", priority " + helper.getPriority());
}
}An unnamed thread gets a default name (Thread-0, Thread-1 …). Before start() it is in state NEW: just an object on the heap, not yet running.
Concurrency vs parallelism
| Concurrency | Parallelism | |
|---|---|---|
| Meaning | Many tasks in progress, taking turns | Many tasks running at the same instant |
| Needs | Even 1 core | 2 or more cores |
| Helps when | Tasks wait for network, disk, users | Tasks do heavy calculations |
| Canteen picture | One cook switching between three dishes | Three cooks, three dishes |
How many cores can your program use? Ask the runtime (your number will differ):
public class Main {
public static void main(String[] args) {
System.out.println("CPU cores Java can use: " + Runtime.getRuntime().availableProcessors());
}
}More threads isn't always faster. If you start 1,000 threads for heavy calculations on an 8-core machine, they just take turns, and switching between them costs time and memory (each thread has its own stack). For many tasks, use a thread pool (Lesson 12.6).
From java Main to the last thread
The OS starts a process
Runningjava Mainmakes the operating system create a new process with its own memory. The JVM sets up the heap and the method area inside it.The JVM creates the main thread
It also starts its own helper threads (garbage collection, JIT compilation). The main thread gets a stack and callsmain(args): its first frame.Your code creates more threads
Thread t = new Thread(() -> download("menu.json")); t.start();new Thread(…)only creates an object on the heap.start()creates the real thread: a new stack, registered with the scheduler.The scheduler shares the cores
The OS decides which thread runs on which core and for how long, switching between them many times a second. You cannot predict the exact order: that's why output from different threads can interleave differently each run.Threads end; then the JVM exits
A thread ends when itsruncode returns. When the last non-daemon thread (oftenmain) ends, the JVM stops any daemon threads and the process exits.
One worker vs three workers: both views
Left: the timeline your users feel. Right: inside the process, the shared heap, one stack per thread, and the CPU cores.
- One thread does one thing at a time:
download(a)must finish beforedownload(b)starts. - The second download waits for the first. The app feels stuck.
- Three one-second downloads take three seconds in a row.
- With three threads, the three downloads run at the same time.
join()makesmainwait until each thread has finished.- When the work is mostly waiting (network, disk), threads finish in about the time of the slowest one. (Times are illustrative.)
- Your program runs as one process: it has its own memory. Inside, the
mainthread has its own stack; objects live on the shared heap. - With one thread, only one core works on your code at any moment.
- On a multi-core machine, the other cores sit idle for this program.
new Thread(…).start()creates threads with their own stacks in the same process. Creating a thread is much cheaper than starting a new process.- All threads see the same heap, so they can fill the same array. The OS scheduler shares the cores between threads, switching between them many times a second.
- When a thread's work ends, its stack is freed. The results stay on the heap for
mainto read.
Threads in real systems
Almost every serious Java program uses several threads, even when you don't create them yourself.
Watch it with an example: a banking app that never freezes
A customer taps “Download statement”. Creating the PDF takes a few seconds. Watch what happens with and without a background thread.
- The customer taps Download statement.
- If the UI thread builds the PDF itself, it can't redraw the screen or react to taps for 4 seconds: the app looks hung.
- Instead, the UI thread starts a background thread for the slow job and returns at once.
- While the PDF is being built, the customer can still check the balance: two threads, one app.
- When the background thread finishes, the app shows “statement ready”. Mobile and desktop apps work this way. (Simplified: real apps use thread pools and UI-safe callbacks, Lesson 12.6.)
1. Responsive apps
Mobile and desktop apps keep one thread for the screen and push slow work (network calls, files, image processing) to background threads, so the app never looks frozen.
2. Web servers
A server handling bookings or payments serves many customers at once, typically handing each incoming request to a thread from a pool.
3. Batch jobs that use every core
Month-end jobs (interest for lakhs of accounts, report generation) split the data into chunks and process the chunks in parallel on all cores.
4. Background helpers
Autosave, cache cleanup, metrics reporting and the JVM's own garbage collector run on helper (often daemon) threads while the main work goes on.
Examples are simplified. Real apps usually use thread pools and frameworks rather than creating raw threads by hand (Lesson 12.6).
Three downloads at once
public class Main {
static String[] files = {"menu.json", "offers.json", "photo.jpg"};
static int[] bytes = new int[3];
static void download(int i) {
bytes[i] = files[i].length() * 100; // pretend work: size depends on the name
System.out.println(Thread.currentThread().getName() + " downloaded " + files[i]);
}
public static void main(String[] args) throws InterruptedException {
Thread[] workers = new Thread[3];
for (int i = 0; i < 3; i++) {
int k = i;
workers[i] = new Thread(() -> download(k), "dl-" + k);
workers[i].start();
}
for (Thread t : workers) {
t.join();
}
System.out.println("All done: " + bytes[0] + " + " + bytes[1] + " + " + bytes[2] + " bytes");
}
}The three “downloaded” lines may come out in a different order each time you run it: the scheduler decides. The last line is always the same, because main waits with join() before printing.
int k = i;A lambda can only use local variables that never change.ichanges every loop;kis a fresh copy per thread.new Thread(() -> download(k), "dl-" + k)The code to run, and a name that shows up in logs.workers[i].start()Creates the real thread. The loop continues immediately; it doesn't wait.t.join()Waits for each worker before readingbytes.
main starts a worker and waits for its answer
This trace is recorded from the real JVM and follows the main thread. Watch the Thread object on the heap change state, and the shared array receive a value written by the other thread.
Worked examples: real problems, solved and traced in memory
Three small programs in the style of real apps. The traces follow the main thread: watch Thread objects appear on the heap, change state, and leave their results in shared objects.
Prepare three menu photos at once
Problem statement
A food app must resize three dish photos before showing the menu. Do each one in its own thread, wait for all three, then print the new sizes. (Resizing is simulated by halving a number.)
photo sizes in KB: 840, 620, 980
420 310 490 KB
Approach
- Store the inputs and the results in two arrays on the heap.
- Create one thread per photo; each thread writes only its own result slot, so they never touch the same element.
- Start all threads, then
join()each one before reading the results.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Two threads fill one report object
Problem statement
A month-end report has two sections: total deposits and total withdrawals. Compute each section in a separate thread that writes into the same Report object, then print the report.
deposits 5000, 12000, 800; withdrawals 2000, 450
In 17800, out 2450, net 15350
Approach
- Create one
Reportobject on the heap; both threads get a reference to it. - Thread A sums deposits into
report.in; thread B sums withdrawals intoreport.out. Different fields, so no clash. - Join both threads, then print. The report object shows both threads' work.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
A daemon autosave thread doesn't keep the app alive
Problem statement
A notes app runs an autosave helper in the background forever. When the user closes the app, the program must end without waiting for the helper. Mark the helper as a daemon thread and show that the program still ends.
(no input)
autosave daemon? true main is done; the JVM exits now
Approach
- Create the autosave thread with an endless loop that sleeps between saves.
- Call
setDaemon(true)beforestart(). - When main ends, only daemon threads are left, so the JVM exits.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 12.1 in one screen
- Process = running program with its own memory. Thread = path of execution inside a process.
- Threads share the heap and static fields; each has its own stack and program counter.
- Every Java program starts on the
mainthread; the JVM also runs GC and JIT threads. new Thread(…)makes an object (NEW);start()makes it run;join()waits for it.- Concurrency = taking turns; parallelism = truly simultaneous on several cores.
- The scheduler picks the order: output from threads can interleave differently each run.
- The JVM exits when all non-daemon threads end; daemon threads are stopped.
Interview and viva questions
What is the difference between a process and a thread?
A process is a running program with its own isolated memory and resources. A thread is a unit of execution inside a process. Threads of the same process share heap memory and resources, which makes communication easy and creation cheap, but each thread has its own stack and program counter.
What do threads share, and what do they not share?
Shared: heap objects, static fields, loaded classes, open files. Not shared: the stack (method frames, local variables, parameters) and the program counter. That's why local variables are naturally thread-safe and shared objects are not.
What is the difference between concurrency and parallelism?
Concurrency means multiple tasks make progress in overlapping time periods, possibly by taking turns on one core. Parallelism means tasks execute at the same instant on multiple cores. Parallelism is one way to achieve concurrency.
What is a daemon thread?
A low-priority background thread that does not prevent the JVM from exiting. When only daemon threads remain, the JVM shuts down and stops them abruptly. Use setDaemon(true) before start(); never use daemons for work that must complete.
Why can't you predict the order of output from two threads?
The operating system's scheduler decides when each thread runs and for how long, based on load, cores and timing. To get a guaranteed order you must coordinate threads explicitly, for example with join(), locks or queues.
Practice problems
Start, name and join threads, and let them share results through the heap. Every solution waits with join() before printing, so the output is always the same. The last three are business tasks.
Name of the current thread
Print the name of the thread that runs main.
(none)
Running on: main
Show solution and explanation
public class Main {
public static void main(String[] args) {
System.out.println("Running on: " + Thread.currentThread().getName());
}
}How it works: the JVM runs main on a thread called "main". Thread.currentThread() returns the thread executing the current line.
Start a named thread and wait for it
Create a thread named "greeter" that prints Hello from greeter. Start it, wait for it, then print main ends.
(none)
Hello from greeter main ends
Show solution and explanation
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread g = new Thread(() -> System.out.println("Hello from " + Thread.currentThread().getName()), "greeter");
g.start();
g.join();
System.out.println("main ends");
}
}How it works: the second argument of the constructor names the thread. join() guarantees the greeting is printed before "main ends".
Default thread names
Create two threads without names and print their default names and states.
(none)
Thread-0 NEW Thread-1 NEW
Show solution and explanation
public class Main {
public static void main(String[] args) {
Thread a = new Thread(() -> {});
Thread b = new Thread(() -> {});
System.out.println(a.getName() + " " + a.getState());
System.out.println(b.getName() + " " + b.getState());
}
}How it works: unnamed threads get names Thread-0, Thread-1 … in creation order. Before start() they are in state NEW. Name your threads: it makes logs readable.
Sum 1 to 1000 in two halves
Use two threads: one sums 1–500, the other 501–1000. Each stores its part in a shared array. Print both parts and the total.
1..1000
125250 + 375250 = 500500
Show solution and explanation
public class Main {
static long[] part = new long[2];
public static void main(String[] args) throws InterruptedException {
Thread low = new Thread(() -> { for (int i = 1; i <= 500; i++) part[0] += i; });
Thread high = new Thread(() -> { for (int i = 501; i <= 1000; i++) part[1] += i; });
low.start();
high.start();
low.join();
high.join();
System.out.println(part[0] + " + " + part[1] + " = " + (part[0] + part[1]));
}
}How it works: each thread writes a different array slot, so they never write the same memory. After both joins, main can safely read the results. Splitting work into independent pieces is the basic idea of parallel processing.
Largest value, split across threads
Find the largest value in an array of 8 numbers by giving each half to a different thread, then comparing the two answers.
[14, 92, 33, 7, 61, 88, 45, 19]
Halves: 92, 88; max 92
Show solution and explanation
public class Main {
static int[] data = {14, 92, 33, 7, 61, 88, 45, 19};
static int[] best = new int[2];
static void maxOf(int slot, int from, int to) {
int m = data[from];
for (int i = from + 1; i < to; i++) m = Math.max(m, data[i]);
best[slot] = m;
}
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(() -> maxOf(0, 0, 4));
Thread b = new Thread(() -> maxOf(1, 4, 8));
a.start();
b.start();
a.join();
b.join();
System.out.println("Halves: " + best[0] + ", " + best[1] + "; max " + Math.max(best[0], best[1]));
}
}How it works: a normal method does the work; each thread just calls it with different ranges. Each thread has its own stack, so the local m in one thread can't interfere with the other's.
Store totals in parallel
A chain has 3 stores. Each store's daily bills are in an array. Compute each store's total in its own thread and print a summary.
Pune {450, 1200, 300}; Nagpur {800, 150}; Nashik {990, 60, 75, 300}Pune: Rs 1950 Nagpur: Rs 950 Nashik: Rs 1425
Show solution and explanation
public class Main {
static String[] stores = {"Pune", "Nagpur", "Nashik"};
static int[][] bills = {{450, 1200, 300}, {800, 150}, {990, 60, 75, 300}};
static int[] totals = new int[3];
public static void main(String[] args) throws InterruptedException {
Thread[] t = new Thread[3];
for (int s = 0; s < 3; s++) {
int k = s;
t[s] = new Thread(() -> { for (int b : bills[k]) totals[k] += b; }, stores[s]);
t[s].start();
}
for (Thread x : t) x.join();
for (int s = 0; s < 3; s++) System.out.println(stores[s] + ": Rs " + totals[s]);
}
}How it works: one thread per store, each writing only its own slot. Naming each thread after its store makes logs and thread dumps easy to read in production.
GST for two halves of an order list
Compute 18% GST on 6 order amounts, using one thread for the first three and another for the last three. Print the GST per order in the original order.
[1000, 250, 4999, 120, 760, 3300]
Order 1: GST 180.0 Order 2: GST 45.0 Order 3: GST 899.82 Order 4: GST 21.6 Order 5: GST 136.8 Order 6: GST 594.0
Show solution and explanation
public class Main {
static double[] amount = {1000, 250, 4999, 120, 760, 3300};
static double[] gst = new double[6];
static void tax(int from, int to) {
for (int i = from; i < to; i++) gst[i] = Math.round(amount[i] * 18) / 100.0;
}
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(() -> tax(0, 3));
Thread b = new Thread(() -> tax(3, 6));
a.start();
b.start();
a.join();
b.join();
for (int i = 0; i < 6; i++) System.out.println("Order " + (i + 1) + ": GST " + gst[i]);
}
}How it works: results are stored by index, so the printing order doesn't depend on which thread finished first. Math.round(x * 18) / 100.0 rounds the tax to paise.
Background audit logger that doesn't block shutdown
Start a daemon thread named "audit" that would write logs forever, record two transactions on main, and end the program cleanly.
two transactions
TXN-1 Rs 500 recorded TXN-2 Rs 120 recorded audit is daemon: true
Show solution and explanation
public class Main {
public static void main(String[] args) {
Thread audit = new Thread(() -> {
while (true) {
try { Thread.sleep(10_000); } catch (InterruptedException e) { return; }
}
}, "audit");
audit.setDaemon(true);
audit.start();
System.out.println("TXN-1 Rs 500 recorded");
System.out.println("TXN-2 Rs 120 recorded");
System.out.println(audit.getName() + " is daemon: " + audit.isDaemon());
}
}How it works: the JVM exits when all non-daemon threads have ended. A daemon thread is a helper that never keeps the program alive. Never use daemons for work that must finish (like saving data): they are stopped abruptly.
Real-world scenario questions (asked in company interviews)
Interviewers use threads questions to see whether you understand what runs where, and what can go wrong.
The app hangs on the payment screen
Users report that after tapping “Pay”, the app becomes unresponsive for 5–8 seconds. The code calls the payment gateway directly inside the button's click handler. What's going on, and what would you change?
The interviewer is checking: that you know the UI runs on one thread and slow work must move off it.
Show a strong answer
- The click handler runs on the UI thread. A blocking network call there stops the screen from redrawing or reacting.
- Move the gateway call to a background thread (or a thread pool / async API), show a progress indicator, and update the screen when the result comes back.
- Disable the Pay button while the call is in flight so the user can't pay twice.
Avoid: “Make the network faster.” The design is the problem, not the speed.
One thread per request, 20,000 users
A booking service creates a brand-new thread for every incoming request. During a festival sale traffic jumps to 20,000 concurrent requests and the server runs out of memory. Explain why and suggest a fix.
The interviewer is checking: that threads cost memory and should be pooled.
Show a strong answer
- Each platform thread reserves its own stack (often hundreds of KB to 1 MB). Thousands of threads use gigabytes and spend time switching.
- Use a bounded thread pool with a queue (Lesson 12.6), or Java 21 virtual threads for I/O-heavy work.
- Add back-pressure: reject or queue excess requests instead of crashing.
Avoid: just raising the heap size; stacks aren't even on the heap.
The audit log is sometimes empty
A tool records transactions, and a background thread writes them to an audit file. Sometimes, after the program ends, the last few entries are missing. The background thread was created with setDaemon(true). Why?
The interviewer is checking: JVM exit rules and daemon threads.
Show a strong answer
- When
mainends, only the daemon is left, so the JVM exits immediately and kills it mid-write. - Use a normal thread and signal it to finish, then
join()it; or use an executor and callshutdown()+awaitTermination(). - Flush and close the file in the writer (try-with-resources).
Avoid: adding Thread.sleep at the end of main and hoping it's long enough.
“We added threads and nothing got faster”
A report job that calculates statistics for 50 lakh rows was changed to use 200 threads on a 4-core server. It got slightly slower. Explain.
The interviewer is checking: CPU-bound vs I/O-bound work and core limits.
Show a strong answer
- The work is CPU-bound: only 4 threads can actually run at a time. The other 196 just wait, and switching between them adds overhead.
- For CPU work, use about as many threads as cores (
availableProcessors()), each on a chunk of data. - Many threads help only when they spend most of their time waiting on I/O.
Avoid: “more threads = more speed.” Measure, and match threads to the kind of work.