Threads, and why a server already has many
You are unlikely to write threads by hand in your first job. You are certain to work on code that runs in several of them, because every web server does — and the bug that results from not knowing it is the hardest kind there is: intermittent, unreproducible, and gone by the time you look.
This module is deliberately short. Three lessons: what a thread is, how to break something with two of them, and how to use a pool without causing the problem. It is enough to be safe and to recognise when you are out of your depth, which is the correct position for a foundation course.
The one fact that matters most
Your code already runs in many threads. A Spring Boot application handles each HTTP request on its own thread. So does a servlet container, a message listener, a scheduled job.
That means a field on a shared object — a service, a singleton, a cache — can be read and written by several requests at the same instant, and nothing in the code says so. This is why "is this class thread-safe?" is a question you will be asked in code review, and why the answer for anything holding mutable state is usually "no".
What a thread is
A thread is an independent path of execution through your program. They share memory — the same heap, the same objects — which is what makes them useful and what makes them dangerous.
Thread t = new Thread(() -> System.out.println("running on " + Thread.currentThread().getName()));
t.start();
t.join();
start() begins a new thread. join() waits for it to finish.
Do not call run() directly. It compiles, and it runs the body on your
current thread — no concurrency, no error, and a test that passes for the wrong
reason.
| Method | Does |
|---|---|
start() |
Begins execution on a new thread |
join() |
Blocks until that thread finishes |
join(millis) |
Waits, with a limit |
Thread.sleep(d) |
Pauses the current thread |
Thread.currentThread() |
The thread you are on |
isAlive() |
Still running |
interrupt() |
Requests that it stop — see below |
setDaemon(true) |
The JVM will not wait for it at shutdown |
Since Java 21 there is a builder form, and it is what to use:
Thread.ofPlatform().name("report-writer").start(runnable);
Thread.ofVirtual().start(runnable);
Platform threads are expensive
Each one maps to an operating system thread with its own stack — roughly a megabyte of reserved memory — and switching between them costs the kernel real work.
2,000 platform threads : 136 ms
100,000 virtual threads: 620 ms
Fifty times as many virtual threads for under five times the elapsed time, and the platform version would fall over long before it reached a hundred thousand. That is the lesson three story.
The practical consequence: do not create threads per task. A server creating a thread per request falls over under load. That is what thread pools exist for, and what virtual threads changed.
Interruption, which is not stopping
There is no safe way to kill a thread. Thread.stop() existed, was deprecated
for twenty years, and has been removed — it could leave shared data half-modified
with locks held.
What you have is a request:
t.interrupt();
That sets a flag. A thread blocked in sleep, wait or join throws
InterruptedException; a running thread sees Thread.currentThread().isInterrupted()
return true and is expected to notice.
The correct way to handle it, and the reason it is worth showing:
try {
Thread.sleep(Duration.ofMillis(millis));
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore the flag
throw new IllegalStateException(e); // or return
}
Catching InterruptedException and ignoring it is a real bug. The exception
clears the interrupted flag, so swallowing it destroys the only evidence that
someone asked this work to stop — and the shutdown that was waiting for it hangs.
Restore the flag, then return or rethrow.
Visibility, briefly
Two threads sharing a field is not only about interleaved writes. A thread may not see another's write at all, because each CPU core has caches and the compiler may reorder instructions.
private boolean running = true; // read in a loop by one thread, set false by another
That loop may never exit. volatile fixes exactly this case — it guarantees that
writes are visible to other threads and are not reordered:
private volatile boolean running = true;
volatile gives visibility, not atomicity. volatile int count; count++
is still broken, because the increment is three operations. That is the next
lesson.
What not to do
- Do not use
Thread.stop,suspendorresume. Removed or deprecated for good reasons. - Do not call
run()when you meantstart(). - Do not swallow
InterruptedException. - Do not create a thread per request. Use a pool, or virtual threads.
- Do not reach for threads to make something faster without measuring. Most slow code is waiting for a database, and concurrency adds bugs before it adds speed.
- Do not share mutable objects between threads if you can avoid it. Passing immutable records — which is what modules 3 and 8 have been building towards — removes the whole problem.
Check your work
Why does this matter if you never create a thread? A web server runs each
request on its own thread, so any shared mutable state is concurrent whether you
wrote a Thread or not.
What is the difference between start() and run()? start() begins a new
thread; run() executes the body on the current one, with no concurrency and no
error.
Why is a platform thread expensive? It maps to an operating system thread with its own stack, about a megabyte, plus kernel-level context switching.
How do you stop a thread? You ask. interrupt() sets a flag; blocking calls
throw InterruptedException and running code is expected to check. There is no
safe forcible stop.
What is wrong with catching InterruptedException and ignoring it? Catching
clears the flag, so the only record of the request is destroyed and a waiting
shutdown hangs. Restore it with Thread.currentThread().interrupt().
What does volatile guarantee, and what does it not? Visibility of writes
across threads and no reordering. Not atomicity — count++ on a volatile int
is still a race.
What is the simplest way to avoid all of this? Do not share mutable state. Pass immutable records between threads.
Practice 2, the interleaved output. Two threads each printing ten lines
produce a different order on every run, and often two lines interleaved
mid-sentence, because System.out.println is atomic per call but your two calls
are not one unit. Running it ten times and getting ten different orders is the
demonstration — nothing is wrong, and nothing is predictable.
Practice 4, the loop that will not exit. With a plain boolean flag the
reader thread may spin forever after the writer sets it false, because it is
reading a cached value. Adding volatile fixes it. Whether it hangs on your
machine depends on the JIT, the core count and the phase of the moon — which is
exactly what makes this class of bug expensive, and why the fix is a keyword
rather than a test.
Practice
-
Start two threads. Each prints its name five times with a short sleep. Use
join()somainwaits. Then remove thejoin()and see what changes. -
Watch the order change. Have two threads each print ten numbered lines. Run it ten times. Write down how many different orderings you saw.
-
Call
run()by mistake. Replacestart()withrun()and printThread.currentThread().getName()inside. Confirm no new thread was created. -
Make a loop that will not exit. One thread spins on a plain
booleanfield while another sets it false after a second. If it exits, try it with the loop body empty and no printing. Then addvolatile. -
Handle interruption properly. Start a thread that sleeps in a loop, interrupt it from
main, and make it exit cleanly while restoring the flag. Then swallow the exception instead and confirm the thread keeps going. -
Harder — a stoppable worker. Write a
ReportWorkerthat processes rows from a queue until asked to stop, where "asked to stop" is an interrupt. It must finish the row it is on, not start another, and report how many it completed. Then makemaininterrupt it halfway and print the count. Getting a worker to stop cleanly is most of what thread management is in practice.
Next: breaking a counter on purpose, which is the fastest way to understand why any of this matters.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship