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 immediatelystart() 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+ overloadThree 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 (declarethrows) or catch and re-arm the flag withThread.currentThread().interrupt(). - It does not release locks. A sleeping thread holds every lock it held before. If you
Thread.sleepinside asynchronizedblock, no other thread enters the block while you sleep. That's almost always a bug; usewaitorCondition.awaitwhen 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 finishedjoin 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 flagThe 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:
- Check
Thread.currentThread().isInterrupted()between long-running steps. - In every
catch (InterruptedException e), either propagate or re-set the flag withThread.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 elseA 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 InterruptedExceptiongetName() 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 changesYou 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.
What to take from the run:
- The
bad.run()line printedran on: main. No new thread was created.bad.isAlive()wasfalseafterwards becausestart()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 thoughslowwould have slept for 2000.isAlive()was stilltrue.join(ms)is the bounded wait — useful when you want to give a worker a graceful chance to finish before escalating. slow.interrupt()ended itsThread.sleepimmediately by throwingInterruptedExceptioninside the worker. That's the contract: interruptible blocking calls react tointerrupt()by bailing out with the exception, which is how cooperative cancellation works in practice.- The
bookkeeperworker caughtInterruptedExceptionand re-armed the flag withThread.currentThread().interrupt(). The subsequentisInterrupted()returnedtrue. 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 beforestart()— calling it after would have thrownIllegalThreadStateException. And whenmainreturned, 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
You catch `InterruptedException` in a worker but don't want to throw out of the loop. What should you do with the interrupt flag?