AlgoMaster Logo

Thread Methods

High Priority22 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

A Thread is more than a constructor and a run method. The day-to-day work happens through a handful of methods on the Thread class that let you start work, pause it, wait for it, signal it to stop, and check what it's doing. This lesson covers the methods you'll actually call: start and why you never call run directly, Thread.sleep for timed pauses, join for waiting on another thread, Thread.yield and what it does (and doesn't) guarantee, the interruption trio (interrupt, isInterrupted, interrupted), the common pitfall of swallowing InterruptedException, isAlive for liveness checks, setDaemon for background threads, and Thread.currentThread() for asking "which thread am I in?".

A running e-commerce example: a background worker that polls an order queue, sleeps between checks, and shuts down cleanly when the main thread signals it.

start vs run: The Most Common Beginner Trap

Thread.start() is the method that actually creates a new operating-system thread and runs your code on it. Thread.run() is just a regular method that contains the code to execute. Calling run directly doesn't start a new thread. It executes the body on the thread that made the call, which is almost never what you want.

The worker prints order-worker because start spun up a fresh thread and ran the lambda on it. Compare what happens if we call run instead.

The lambda ran on main. No new thread was created. worker.run() is a regular method call. The worker Thread object exists, but it never moved out of the NEW lifecycle state because start was never called.

The diagram shows the split. start is the path that creates a new thread of execution. run is a synchronous method call. If main shows up in places where you expected a worker name, the first thing to check is whether someone called run instead of start.

A second rule goes with this one: you can only call start once on a Thread object. Calling it twice throws IllegalThreadStateException.

Once a thread has finished, it's done. The Thread object can't be reused. To run the same work again, build a new Thread (or use an executor). This restriction is why people lean on thread pools for repeated work instead of recreating Thread instances by hand.

Thread.currentThread(): Knowing Which Thread You Are In

Thread.currentThread() is a static method that returns the Thread object representing whichever thread is executing the call. It's the answer to "where am I?" inside any method, and the first tool to use when something prints from an unexpected thread.

The exact id will vary between runs and JVM versions, but the shape is the same. From the same describe() method, two different threads see two different identities, because currentThread resolves to the calling thread each time.

This method is also the gateway to the interruption methods. To signal yourself, write Thread.currentThread().interrupt(). To check whether you've been signaled, write Thread.currentThread().isInterrupted(). The static Thread.interrupted() is even shorter, since it implicitly works on the current thread.

Thread.currentThread() is cheap (a JNI call backed by a thread-local), so calling it inside a tight loop is fine. The cost only matters when you call it millions of times per second; cache it in a local if you do.

Thread.sleep: Pausing the Current Thread

Thread.sleep is a static method that pauses the calling thread for at least the requested number of milliseconds. An overload takes a nanosecond component too, but the millisecond version is what you'll use 99% of the time. While a thread is sleeping, the JVM moves it from RUNNABLE to TIMED_WAITING and doesn't schedule it again until the time elapses or someone interrupts it.

The elapsed time will be 500 ms or slightly more, depending on the OS scheduler. sleep makes a minimum-time guarantee, not a maximum. If the system is busy, you might wake up a few milliseconds late. You won't wake up early unless someone interrupts you.

Three things to know about sleep:

BehaviorDetail
It's staticThread.sleep(...), not someThread.sleep(...). You can only sleep the current thread.
It throws checked InterruptedExceptionIf another thread calls interrupt() on this one during the sleep, sleep throws and clears the interrupt flag.
It releases the CPU but not locksIf the thread is holding a synchronized lock, that lock stays held while it sleeps.

The third row is the surprising one. sleep isn't a polite "step aside so others can take my locks" mechanism. It stops scheduling this thread for a while. Any synchronized resource the thread holds is still held when it wakes up.

The first overload is Thread.sleep(long millis). Thread.sleep(long millis, int nanos) also exists for sub-millisecond precision, but most platforms only honor a few hundred nanoseconds of resolution and the millisecond version is good enough for nearly every case in application code.

In our e-commerce setting, sleep is what an order-polling worker uses between queue checks. A 500 ms gap between polls reduces load on the queue without blocking new orders for long:

Each sleep(500) pauses the loop for at least half a second. The whole program runs in about 1.5 seconds, give or take scheduler jitter.

join: Waiting for Another Thread to Finish

join is how one thread waits for another to terminate. You call someThread.join() from the thread that needs to wait, and the call blocks until someThread enters the TERMINATED state. It's the standard way to make sure background work has finished before the main thread moves on.

Without the join, the last line could print before "Inventory check complete", because main would race ahead. join synchronizes them: main parks itself until inventory is done, then resumes.

join has three overloads:

SignatureBehavior
join()Waits indefinitely until the target thread terminates.
join(long millis)Waits up to millis milliseconds. Returns whether or not the target finished.
join(long millis, int nanos)Same as above, with nanosecond precision.

The bounded form is important. A pure join() will block forever if the target thread hangs, which is the kind of bug you don't want in a service shutdown path. A join(5000) blocks for at most 5 seconds and then returns whether the target finished or not. Check isAlive() afterward to find out.

main waits half a second, sees the import is still going, and continues. The import thread will eventually finish, but main doesn't block on it. In a real e-commerce service, this pattern is how you implement "wait up to N seconds for the cache to warm, then start serving requests regardless".

The diagram shows the join handshake. main parks at the join call and doesn't resume until the worker reaches TERMINATED. The two threads run concurrently between start and join; the join is the synchronization point at the end.

Like sleep, join throws InterruptedException if another thread interrupts the waiting one.

Thread.yield: A Suggestion, Not a Command

Thread.yield() is a static method that tells the scheduler "I am willing to give up my time slice if another thread of the same priority wants to run." The keyword is "willing". The scheduler is free to ignore the hint and keep running the same thread. On modern JVMs and modern operating systems, yield is closer to a polite cough than a real handoff.

Sample output (will vary across runs):

That's one possible interleaving. You might also see A 0 A 1 B 0 A 2 B 1 B 2 or A 0 A 1 A 2 B 0 B 1 B 2 or any other ordering. yield doesn't enforce alternation. It's a hint that the scheduler may or may not act on.

The honest truth about yield:

  • It has weak guarantees. The JVM spec leaves it deliberately vague.
  • Different JVM versions and operating systems treat it differently. Some optimize it to a no-op.
  • It doesn't release locks (same as sleep).
  • It doesn't throw InterruptedException.

In application code, you almost never need yield. To pause and let others run, use Thread.sleep. To wait on a condition, use one of the proper synchronization primitives (locks, condition variables, semaphores). The places yield shows up legitimately are in tight spin-wait loops where you would otherwise burn 100% CPU, and even there it's rarely the best answer.

The yield inside the spin loop keeps the loop from monopolizing a CPU core if there's another runnable thread waiting. That said, a real implementation would use wait/notify or a CountDownLatch instead of spinning. yield is a code smell more often than it is appropriate.

A yield is cheaper than a sleep(1) because it doesn't park the thread. The downside is it can also be a no-op, which means a tight while (!done) Thread.yield(); loop can still hit 100% CPU on some platforms.

interrupt, isInterrupted, and interrupted: The Signaling Trio

Java doesn't have a way to forcibly stop a thread. There used to be Thread.stop(), but it was deprecated decades ago because it could leave shared data in a corrupt state. The modern approach is cooperative cancellation: one thread sets a flag, and the other thread checks the flag and decides when to exit.

The flag is called the interrupt status, and three methods work with it:

MethodStatic or instance?Effect
someThread.interrupt()instanceSets the interrupt flag on someThread. If someThread is blocked in sleep, join, or wait, it wakes up and throws InterruptedException (and the flag is cleared).
someThread.isInterrupted()instanceReads the flag without changing it. Returns true if the thread has been interrupted.
Thread.interrupted()staticReads the flag of the current thread and clears it. Returns true if the current thread had been interrupted.

The static Thread.interrupted() is the subtle one. It both reads and clears the flag. That's deliberate: it's designed for places where the current thread has handled the interruption and wants to reset the flag for the next round of work. Confusing it with isInterrupted() is one of the most common bugs in this area.

A worker thread that polls an order queue and shuts down cleanly when interrupted.

A few things are working together here. The loop condition reads Thread.currentThread().isInterrupted(), so the worker exits when the flag is set. When the worker is sleeping at the moment of interruption, sleep throws InterruptedException and clears the flag, so the catch block re-sets it with Thread.currentThread().interrupt() before breaking out. Both paths converge on a clean shutdown.

The diagram shows the two paths an interrupt can take. If the worker is in the middle of work (Running), the flag is just set and the next loop check sees it. If the worker is sleeping (TIMED_WAITING), the sleep call throws InterruptedException and clears the flag, so the catch block restores it before exiting. Either way, the worker ends up out of its loop and into termination.

Handling InterruptedException Correctly

InterruptedException is a checked exception thrown by sleep, join, wait, and many other blocking calls when the thread is interrupted while waiting. Handling it correctly is subtle, so it gets its own section.

The rule of thumb: when you catch InterruptedException, you must do one of two things. Either propagate the exception, or restore the interrupt flag before continuing. If you do neither, you've lost the signal that someone wanted this thread to stop.

What's wrong with this code?

Sample output:

The worker keeps polling forever. When main calls interrupt, the flag is set, but the worker is in sleep at that moment, so sleep throws InterruptedException and clears the flag. The empty catch block does nothing. On the next loop iteration, isInterrupted() reads false (the flag was cleared by the throw), so the loop keeps going. The signal was swallowed.

Fix: Restore the interrupt flag inside the catch block.

The catch block re-sets the flag with Thread.currentThread().interrupt(). The next loop check sees the flag is set, and the loop exits. The worker terminates cleanly.

The two acceptable patterns for InterruptedException:

PatternWhen to use it
Re-throw the exceptionMethods that already declare throws InterruptedException should let callers handle it.
Restore the flag with Thread.currentThread().interrupt()Methods that can't throw (e.g., Runnable.run(), which has no throws clause) must restore the flag and then handle locally.

The pattern you must never use is the empty catch. An empty catch on InterruptedException is a real bug, not a style preference.

A small subtlety: some codebases write catch (InterruptedException e) { return; } inside a Runnable.run() body to exit the worker without restoring the flag. That's defensible when the run method is about to terminate anyway, because there's no remaining work that needs to see the flag. The conservative habit is still to restore the flag and break out, so callers higher up in the stack (if any) can also react.

isAlive: Checking If a Thread Is Still Running

isAlive() returns true if the thread has been started and hasn't yet finished. It's the public API for "is this thread between start and termination?". Use it after a bounded join to find out whether the wait completed or timed out.

A thread is "alive" only between start and termination. Before start, it's in the NEW state and isAlive returns false. After termination, it's in the TERMINATED state and isAlive returns false again. Don't confuse "alive" with "currently running"; a thread in BLOCKED, WAITING, or TIMED_WAITING is still alive.

isAlive is also useful for diagnostic prints in shutdown paths. After a bounded join, an if (worker.isAlive()) check tells you whether the join timed out without having to track that separately.

Daemon Threads: setDaemon and Background Work

Java threads come in two flavors: user threads and daemon threads. The difference is what happens at JVM shutdown. The JVM exits when all user threads have finished, regardless of daemon threads. If only daemon threads are still running, the JVM exits and the daemons are killed where they stand, without their finally blocks necessarily running.

You mark a thread as a daemon by calling setDaemon(true) before start. Calling it after start throws IllegalThreadStateException.

Sample output:

The heartbeat thread has an infinite loop and would print forever if it were a user thread. Because it's a daemon, when main finishes the JVM exits and the daemon is terminated. You might see one extra heartbeat after the "exiting" line depending on timing, but the program won't hang.

Without setDaemon(true), the same code would print heartbeat forever, because the JVM would wait for the user-thread heartbeat loop to finish, which it never does.

Daemon?When the JVM exitsTypical use
false (user)When all user threads finishMain application work, request handlers, things you don't want killed mid-write
true (daemon)When all user threads finish (daemons get killed)Background heartbeats, metrics emitters, cache eviction loops, things that don't need a clean shutdown

The trap with daemons is that they don't run their `finally` blocks when killed. If your daemon writes to a log file and you rely on a finally to flush, those last writes can be lost on shutdown. Don't put cleanup code that has to run inside a daemon. Use a regular thread plus explicit interrupt + join for anything that owns state you care about.

The diagram lays out the shutdown decision. When the last user thread is done, the JVM checks for other user threads. If there are none, it exits, and the daemons go with it. The daemons don't get a chance to wrap up.

A common e-commerce use of daemons is a metrics reporter that periodically prints how many orders have been processed. Losing the last report when the service shuts down is fine; it's a metric, not a transaction. A daemon thread is the right fit.

Tying It Together: A Worker That Polls and Shuts Down Cleanly

A small program that combines most of what we've covered. A main thread starts a worker that polls an in-memory order queue, sleeps between polls, prints what it sees, and shuts down cleanly when main signals interruption.

Sample output (timing-dependent):

The poller uses isInterrupted() as its loop condition, sleep to space out polls, and catches InterruptedException so the sleep wakes up cleanly when main signals stop. main uses start, interrupt, and a bounded join to coordinate. A small synchronized block wraps the queue access; it's there so the example is honest about thread-safe queue access, even though synchronization isn't the focus of this lesson.

The shutdown pattern in main, interrupt then bounded join, is the canonical way to stop a background worker in Java. It works because the worker is written to respect interruption. A worker that ignores interrupts would defeat this whole setup.

Quiz

Thread Methods Quiz

10 quizzes