W3docs

Java Thread Methods

Common Java thread control methods — start, run, sleep, join, interrupt, yield, setDaemon — with the gotchas that bite real code.

Java Thread Methods

Thread has a lot of methods on it, and only a handful are ones you'll call in real code. This chapter goes through that handful — what each one does, what it doesn't do, and the mistakes that look correct until they're wrong. The earlier chapters introduced these in passing; here they get pinned down individually.

start() vs. run()

The single most common multithreading bug:

Thread t = new Thread(() -> work(), "worker");
t.run();                                      // wrong: runs work() on the CURRENT thread
t.start();                                    // right: spawns a new OS thread, returns immediately

start() is the only method that creates a new OS thread. run() is the body of the work — calling it directly is just a normal method invocation that returns when the work finishes. If you don't see start(), no parallelism is happening.

start() is also single-use. After run() returns, the thread is TERMINATED and cannot be restarted. A second call to start() throws IllegalThreadStateException.

Thread.sleep(ms)

The static call that parks the current thread for at least the given duration:

Thread.sleep(1500);                          // sleep 1.5 seconds
Thread.sleep(0, 250);                        // 250 nanoseconds; precision varies by OS
Thread.sleep(Duration.ofMillis(1500));       // Java 19+ overload

Three things to know:

  • It throws InterruptedException. Sleep is interruptible — that's how a worker is told to stop sleeping and shut down. You either propagate the exception (declare throws) or catch and re-arm the flag with Thread.currentThread().interrupt().
  • It does not release locks. A sleeping thread holds every lock it held before. If you Thread.sleep inside a synchronized block, no other thread enters the block while you sleep. That's almost always a bug; use wait or Condition.await when you need to release the lock.
  • The timing is "at least," not "exactly." The OS might wake you a tick late under load; it never wakes you a tick early.

t.join() and t.join(ms)

Wait for another thread to finish:

t.join();                                    // block until t terminates
t.join(2000);                                // block up to 2 seconds, then continue regardless
boolean done = t.join(Duration.ofSeconds(2));// Java 19+, returns whether it finished

join is how you compose multi-step parallel work: spawn a few threads, let them run, join them all, read their results. join() returns when the target thread's run() has returned (whether normally or by exception). It also throws InterruptedException so callers can be interrupted out of the wait.

A subtle one: join(0) means "join with no timeout" (i.e. wait forever), not "join with a zero timeout." If you want a real "give up immediately" call, use t.isAlive() instead.

t.interrupt() and the flag

The cooperative cancel protocol, in three calls:

t.interrupt();                               // set t's interrupt flag (and unblock sleep/wait/join/park)
t.isInterrupted();                           // ask whether the flag is set (does NOT clear)
Thread.interrupted();                        // static; ask current thread, and CLEAR the flag

The flag is just a volatile boolean on the Thread object. interrupt() sets it. If t is currently in sleep, wait, join, or LockSupport.park (or many java.nio blocking calls), that blocking call throws InterruptedException immediately. Otherwise the flag waits for the worker to notice on its own.

A worker that wants to be interruptible has two responsibilities:

  1. Check Thread.currentThread().isInterrupted() between long-running steps.
  2. In every catch (InterruptedException e), either propagate or re-set the flag with Thread.currentThread().interrupt() — never swallow it silently.
while (!Thread.currentThread().isInterrupted()) {
  try {
    doOneUnit();
    Thread.sleep(100);
  } catch (InterruptedException e) {
    Thread.currentThread().interrupt();        // restore flag for the loop check
  }
}

Thread.yield() — almost never the right tool

Thread.yield();                              // hint: please run someone else

A non-binding hint to the scheduler. The OS is free to ignore it. There is essentially no production code that needs yield — if you want to wait for an event, use a wait, a Condition, or a Semaphore. Reach for yield only for micro-benchmarks, deadlock-testing harnesses, or when you're writing the JVM itself.

Thread.currentThread()

The static accessor for the thread the calling code is on. The two uses you'll see:

String who = Thread.currentThread().getName();
Thread.currentThread().interrupt();          // re-arm the flag after catching InterruptedException

getName() is also the standard way to label log lines so you can tell threads apart in production output.

getName / setName

Names matter for debugging. The default name (Thread-3) is useless in a thread dump.

Thread t = new Thread(this::flush, "flush-loop");      // name at construction (preferred)
t.setName("flush-loop-2");                              // rename later if a role changes

You can rename anytime, but the value at the moment of dump or log is what the reader will see. Always pass a name to the constructor.

setDaemon(true)

Thread t = new Thread(this::poll, "metrics-poller");
t.setDaemon(true);                           // BEFORE start(); else IllegalThreadStateException
t.start();

Daemon threads don't keep the JVM alive — when the last non-daemon thread exits, the JVM yanks daemons out from under. Use them for housekeeping that should die with the program (timers, metric flushers, polling loops). Don't use them for work whose completion you actually need.

setPriority(int)

t.setPriority(Thread.MAX_PRIORITY);          // 10
t.setPriority(Thread.MIN_PRIORITY);          // 1
t.setPriority(Thread.NORM_PRIORITY);         // 5 (default)

Mostly advisory. The next chapter covers priorities in detail; for now, the headline: don't rely on them for correctness, the OS decides what they mean.

Thread.holdsLock(obj)

A static debug helper:

assert Thread.holdsLock(monitor) : "expected to be inside a synchronized block on monitor";

Returns true if the calling thread holds the intrinsic monitor of obj. Useful for asserting "this method must only be called from inside a synchronized block" without paying lock-acquisition cost on the happy path.

Thread.onSpinWait() — Java 9+

while (!done) {
  Thread.onSpinWait();                       // hint to the CPU: I'm spinning, slow down
}

A CPU-level hint that pauses pipelines and reduces power use during a tight spin loop. It's specifically for the very narrow case where you're spinning a few microseconds waiting for another thread to flip a flag; it's not a general "give up CPU" call. For anything longer, use LockSupport.park or a Condition.

A worked example: most of these in one place

The program below uses start, join with a timeout, interrupt, sleep, isInterrupted, and setName together — the methods you'd actually call in production.

java— editable, runs on the server

What to take from the run:

  • The bad.run() line printed ran on: main. No new thread was created. bad.isAlive() was false afterwards because start() was never called. Every multithreading program at some point has this bug; once you've made it, you never make it again.
  • The slow.join(300) returned after about 300 ms even though slow would have slept for 2000. isAlive() was still true. join(ms) is the bounded wait — useful when you want to give a worker a graceful chance to finish before escalating.
  • slow.interrupt() ended its Thread.sleep immediately by throwing InterruptedException inside the worker. That's the contract: interruptible blocking calls react to interrupt() by bailing out with the exception, which is how cooperative cancellation works in practice.
  • The bookkeeper worker caught InterruptedException and re-armed the flag with Thread.currentThread().interrupt(). The subsequent isInterrupted() returned true. Without that re-arm, the flag is lost and any code further up the call stack thinks no interrupt ever happened.
  • daemon.setDaemon(true) was called before start() — calling it after would have thrown IllegalThreadStateException. And when main returned, the daemon was killed mid-sleep; the JVM exited because no non-daemon thread remained. That's the daemon trade-off: never blocks JVM exit, never guaranteed to finish.

What's next

The next chapter, Java Thread Priority, covers the setPriority method on Thread, what the priorities actually do on real OSes, and why you should treat them as a hint rather than a guarantee.

Practice

Practice

You catch `InterruptedException` in a worker but don't want to throw out of the loop. What should you do with the interrupt flag?