RizTech Academy logo
RizTech Academy
ConcurrencyLesson 1 of 825 min

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, suspend or resume. Removed or deprecated for good reasons.
  • Do not call run() when you meant start().
  • 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

  1. Start two threads. Each prints its name five times with a short sleep. Use join() so main waits. Then remove the join() and see what changes.

  2. Watch the order change. Have two threads each print ten numbered lines. Run it ten times. Write down how many different orderings you saw.

  3. Call run() by mistake. Replace start() with run() and print Thread.currentThread().getName() inside. Confirm no new thread was created.

  4. Make a loop that will not exit. One thread spins on a plain boolean field while another sets it false after a second. If it exits, try it with the loop body empty and no printing. Then add volatile.

  5. 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.

  6. Harder — a stoppable worker. Write a ReportWorker that 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 make main interrupt 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