Thread lifecycle

A thread isn't simply “running” or “not running”. It is born, waits for the CPU, sleeps, waits for locks and for other threads, and finally ends. Java names these moments with six states. You'll see each one happen in real code, learn the methods that move a thread between them, and learn how to cancel a thread politely with interrupt().

Module 12 · Multithreading⏱ 22 minIntermediate9 practice problems
1 The story

“Is my thread stuck or just sleeping?”

Riya's canteen app has a background thread that checks the payment status every few seconds. A customer complains the app is “stuck”. Riya stares at her code: is the thread dead, sleeping, or waiting for something?

NEW RUNNABLE sleeping waiting for others TERMINATED t.getState() tells you where a thread is right now myphatshala.in
Riya

Sir, how do I know what my thread is doing right now?

Arjun Sir

Ask it. t.getState() returns one of six states. A sleeping thread says TIMED_WAITING, a stuck one is often BLOCKED or WAITING, and a dead one says TERMINATED.

Riya

And if it's sleeping but the user wants to cancel?

Arjun Sir

Then you interrupt() it. It wakes up early and can stop cleanly. Let's go through the whole life of a thread.

2 Definition

What is the thread lifecycle?

Formal definition (write this in exams)

The thread lifecycle is the sequence of states a thread passes through from creation to termination. In Java these are the constants of the enum Thread.State: NEW (created, not started), RUNNABLE (executing or ready to execute), BLOCKED (waiting to acquire a monitor lock), WAITING (waiting indefinitely for another thread, e.g. join(), wait()), TIMED_WAITING (waiting for a specified time, e.g. sleep(ms), join(ms)) and TERMINATED (completed). A thread's current state is returned by getState().

In simple words

Like a delivery rider's day: hired (NEW), on the road or waiting at a signal (RUNNABLE), on a timed break (TIMED_WAITING), waiting for the restaurant to hand over food (WAITING), queued at the one-person counter (BLOCKED), and shift over (TERMINATED).

Words you'll meet in this lesson

Thread.StateThe enum with the six states.
sleep(ms)Pauses the current thread for a time. Uses no CPU.
join() / join(ms)Wait for another thread to finish (optionally with a limit).
interrupt()Sets a thread's interrupted flag: a polite “please stop”.
InterruptedExceptionThrown by sleep/join/wait when the thread is interrupted.
isAlive()True between start() and termination.

The six states at a glance

StateHow it gets thereHow it leaves
NEWnew Thread(…)start()
RUNNABLEstart(), or a wait endsa wait, a lock, or run() ends
TIMED_WAITINGsleep(ms), join(ms), wait(ms)time up, or interrupted
WAITINGjoin(), wait()other thread ends / notifies, or interrupted
BLOCKEDentering a synchronized block that's takenthe lock is released
TERMINATEDrun() returns or throwsnever

Exam tip: Java has no RUNNING state; running and ready are both RUNNABLE. Many textbooks draw a “Running” box anyway, so in exams you may write “Runnable (ready) ⇄ Running” and mention that getState() reports both as RUNNABLE.

3 Types and ways to use it

The waiting states, side by side

TIMED_WAITING

sleep(ms), join(ms), wait(ms). Comes back by itself when the time is up.

has a timer

WAITING

join(), wait(). Needs another thread to finish or call notify.

no timer

BLOCKED

Waiting to get a lock another thread holds. Only synchronized causes it.

lock queue

RUNNABLE

Running, or ready and waiting for a CPU core. Busy loops stay here and burn CPU.

in the scheduler

All six states in one program

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread sleeper = new Thread(() -> {
            try { Thread.sleep(60_000); } catch (InterruptedException e) { }
        }, "sleeper");
        Thread waiter = new Thread(() -> {
            try { sleeper.join(); } catch (InterruptedException e) { }
        }, "waiter");
        Thread blocked = new Thread(() -> {
            synchronized (lock) { }
        }, "blocked");

        System.out.println("sleeper: " + sleeper.getState());
        sleeper.start();
        waiter.start();
        synchronized (lock) {                 // main holds the lock...
            blocked.start();                  // ...so this thread can't get in
            while (blocked.getState() != Thread.State.BLOCKED
                    || sleeper.getState() != Thread.State.TIMED_WAITING
                    || waiter.getState() != Thread.State.WAITING) {
                Thread.onSpinWait();          // wait until all three have settled
            }
            System.out.println("sleeper: " + sleeper.getState());
            System.out.println("waiter:  " + waiter.getState());
            System.out.println("blocked: " + blocked.getState());
        }
        sleeper.interrupt();
        sleeper.join();
        waiter.join();
        blocked.join();
        System.out.println("sleeper: " + sleeper.getState());
    }
}
sleeper: NEW sleeper: TIMED_WAITING waiter: WAITING blocked: BLOCKED sleeper: TERMINATED

The loop with Thread.onSpinWait() keeps main checking until all three helpers have reached their waiting states, so the output is the same every run. In real code you'd rarely wait like this; here it lets us take a clean snapshot.

The methods that move a thread

Thread.sleep(500);            // pause the CURRENT thread 500 ms  -> TIMED_WAITING
t.join();                     // wait until t ends                 -> WAITING
t.join(200);                  // ...but at most 200 ms             -> TIMED_WAITING
t.interrupt();                // politely ask t to stop
Thread.currentThread().isInterrupted();   // check the flag
t.getState();  t.isAlive();   // inspect
Thread.yield();               // hint: let another thread run (rarely needed)

t.sleep(100) does not put t to sleep. sleep is static, so it always pauses the thread that runs the line. The compiler only warns. Always write Thread.sleep(…).

4 Step by step

How interrupt() cancels a sleeping thread

  1. The worker sleeps

    try {
        Thread.sleep(60_000);
    } catch (InterruptedException e) {
        return;               // stop cleanly
    }
    The worker is TIMED_WAITING. The OS won't schedule it until the minute is up.
  2. Another thread calls interrupt()

    worker.interrupt() sets a boolean interrupted flag inside the worker's Thread object and wakes it if it's sleeping, waiting or joining.
  3. sleep throws

    The worker becomes RUNNABLE, and sleep throws InterruptedException (message "sleep interrupted"). Throwing also clears the flag.
  4. The catch block decides

    Clean up and return, so run() ends and the thread becomes TERMINATED. If you can't stop here, call Thread.currentThread().interrupt() to restore the flag for code further up.
  5. Busy threads must check

    A thread that never sleeps or waits won't throw. Long loops should test Thread.currentThread().isInterrupted() and stop when it's true.
5 See it happen

Six states: both views

Left: the line a thread is on and the state getState() would report. Right: the state diagram with the transition taken.

The life of a thread 6 steps
What you see Thread t = new Thread(task); t.start(); Thread.sleep(500); // in task synchronized (lock) {…} // lock busy other.join(); // in task // run() returns state of t: NEW RUNNABLE TIMED_WAITING BLOCKED WAITING TERMINATED t.getState() returns this value Created, not started. Running or ready to run. Sleeping for a set time. Waiting to enter a lock. Waiting with no time limit. Finished for good. myphatshala.in
Behind the screen NEW RUNNABLE TERMINATED TIMED_WAITING BLOCKED WAITING NEW RUNNABLE TIMED_WAITING BLOCKED WAITING TERMINATED start() sleep(ms), join(ms) lock busy join(), wait() time up / interrupt → back to RUNNABLE lock released → back to RUNNABLE other thread ends / notify → RUNNABLE no way back from TERMINATED enum Thread.State Only an object. In the scheduler's hands. Parked with a timer. Queued for a lock. Parked until someone acts. Done. myphatshala.in
6 Real-world use

Lifecycle in real systems

Knowing the states is how engineers read thread dumps: a snapshot of every thread and its state, taken when a server is slow or stuck.

Watch it with an example: a payment-status poller the user can cancel

After a UPI payment, the app checks the bank every 2 seconds until the status is final. If the user taps Cancel, main interrupts the poller.

Payment poller: sleep, wake, interrupt 6 steps
Animated example Thread "poller" NEW RUNNABLE TIMED_WAITING RUNNABLE TIMED_WAITING TERMINATED woken by interrupt while (true) { check(bank); Thread.sleep(2000); } catch (InterruptedException e) stop polling bank says: PENDING user taps Cancel → main: poller.interrupt() poller woke up early and stopped Poller created. Asks the bank: pending. Sleeps 2 s, no CPU used. Wakes, asks again. User cancels: interrupt. sleep throws; thread ends. myphatshala.in

1. Polling and retries

Payment-status checks, retrying a failed SMS after 5 seconds, refreshing a cache every minute: all use timed waits.

check→sleep(2000)→check again

2. Cancel buttons

Cancel upload, stop export, abort search: the UI thread interrupts the worker, which stops at its next sleep, wait or flag check.

tap Cancel→worker.interrupt()→clean stop

3. Thread dumps in production

jstack or monitoring tools list every thread with its state. Hundreds of BLOCKED threads on one lock point to a bottleneck; WAITING forever points to a missed notify or a deadlock.

server slow→thread dump→find BLOCKED threads

4. Timeouts

Never wait forever for an outside system. join(ms) (and later Future.get(timeout)) lets the app give up and show a fallback.

bank API hangs→join(100)→cached balance

Examples are simplified. Production code usually cancels tasks through executors and futures (Lesson 12.6), which use interrupts underneath.

7 Run the code

A download that sleeps between updates

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread download = new Thread(() -> {
            for (int pct = 25; pct <= 100; pct += 25) {
                System.out.println("download " + pct + "%");
                try {
                    Thread.sleep(40);
                } catch (InterruptedException e) {
                    System.out.println("download cancelled");
                    return;
                }
            }
        }, "download");

        System.out.println("1. " + download.getState());
        download.start();
        download.join();
        System.out.println("2. " + download.getState() + ", alive " + download.isAlive());
    }
}
1. NEW download 25% download 50% download 75% download 100% 2. TERMINATED, alive false
  • download.getState() before start()Always NEW.
  • Thread.sleep(40)Pauses the download thread (not main) between updates: TIMED_WAITING.
  • catch (InterruptedException e)Required: sleep's checked exception can't escape run(). The catch is where cancelling happens.
  • download.join()main is WAITING until the download ends.
8 Line by line

NEW, interrupted, TERMINATED

Recorded from the real JVM, following main. Watch the poller's state on the heap, and see how an interrupt turns a one-minute sleep into an instant stop.

Worked examples: real problems, solved and traced in memory

Three lifecycle tools you'll use constantly: sleep for pauses, interrupt for cancelling, and join(ms) for waiting with a time limit. The traces follow main and show the worker's state on the heap.

Example 1Food delivery · tracking

Order status ticks with sleep()

Problem statement

A delivery tracker moves an order through three statuses, pausing between updates. Run the updates on a tracker thread with Thread.sleep(50) between them. Main waits and then confirms the tracker has ended.

Input
statuses: PLACED, COOKING, OUT FOR DELIVERY
Expected output
Order #88: PLACED
Order #88: COOKING
Order #88: OUT FOR DELIVERY
tracker TERMINATED, alive false

Approach

  1. The tracker loops over the statuses, printing each and sleeping in between.
  2. sleep throws a checked exception, so catch it inside the lambda.
  3. main joins, then checks getState() and isAlive().

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Analytics · reports

Cancel a slow report with interrupt()

Problem statement

A report job processes rows, pausing after each one (simulating slow disk reads). The user cancels right away. The job must notice the interrupt, stop cleanly, and say how many rows it finished.

Input
rows 1..5, cancel immediately
Expected output
cancelled after 1 row(s)
report state: TERMINATED

Approach

  1. The worker processes a row, then sleeps 1 second.
  2. main calls interrupt() straight after start().
  3. The sleep throws InterruptedException; the catch reports progress and returns.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Banking · timeouts

Wait at most 100 ms for a slow bank API

Problem statement

A balance check calls a bank API that sometimes hangs. main waits for it with join(100). If it hasn't finished by then, main reports a timeout, cancels the call with interrupt(), and shows a cached balance.

Input
cached balance Rs 12,400; the API hangs
Expected output
timeout: bank-api is TIMED_WAITING
api call cancelled
showing cached balance Rs 12,400

Approach

  1. The API thread sleeps 60 s to simulate a hung call.
  2. join(100) returns after at most 100 ms, finished or not.
  3. Check isAlive(): if still alive, interrupt it and use the cached value.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 12.3 in one screen

  • Six states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED (Thread.State).
  • RUNNABLE = running or ready; Java has no separate RUNNING state.
  • sleep(ms), join(ms) → TIMED_WAITING; join(), wait() → WAITING; busy lock → BLOCKED.
  • Thread.sleep always pauses the current thread.
  • interrupt() sets a flag; sleep/join/wait then throw InterruptedException and clear the flag.
  • Use join(ms) + isAlive() for timeouts.
  • TERMINATED is final; a thread can't be restarted.

Interview and viva questions

Explain the lifecycle of a thread in Java.

A thread is NEW after construction. start() makes it RUNNABLE (running or ready). It may move to TIMED_WAITING (sleep, timed join/wait), WAITING (join, wait) or BLOCKED (waiting for a monitor lock), and returns to RUNNABLE when the condition ends. When run() completes or throws, it is TERMINATED.

What is the difference between WAITING and BLOCKED?

BLOCKED means the thread wants to enter a synchronized block/method and another thread holds the lock. WAITING means the thread chose to wait (join, wait) until another thread does something. A thread in wait() also releases the lock; a BLOCKED thread holds nothing.

What is the difference between sleep() and wait()?

sleep is a static Thread method that pauses the current thread for a time and keeps any locks it holds. wait is an Object method that must be called while holding that object's lock; it releases the lock and waits for notify or a timeout (Lesson 12.5).

How do you stop a thread in Java?

Cooperatively: call interrupt(), and write the thread so it stops when sleep/wait throws InterruptedException or when isInterrupted() is true. Thread.stop() was deprecated because killing a thread mid-operation can leave shared data corrupted.

What happens to the interrupted flag when InterruptedException is thrown?

It is cleared. If your code catches the exception but can't stop right there, it should call Thread.currentThread().interrupt() to restore the flag so code higher up can see the request.

Practice problems

Observe states, pause with sleep, wait with join, and cancel with interrupt. Every output is fixed. The last three are business tasks.

P1Easy

State before and after

Print a thread's state before start() and after join().

Input
(none)
Expected output
before: NEW
after: TERMINATED
Show solution and explanation
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {});
        System.out.println("before: " + t.getState());
        t.start();
        t.join();
        System.out.println("after: " + t.getState());
    }
}
before: NEW after: TERMINATED

How it works: NEW until start(); after join() returns the thread is certainly TERMINATED. Any state in between depends on timing.

P2Easy

isAlive()

Print isAlive() before starting a thread and after it has finished.

Input
(none)
Expected output
alive before start: false
working
alive after join: false
Show solution and explanation
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> System.out.println("working"));
        System.out.println("alive before start: " + t.isAlive());
        t.start();
        t.join();
        System.out.println("alive after join: " + t.isAlive());
    }
}
alive before start: false working alive after join: false

How it works: a thread is alive from start() until it terminates. NEW and TERMINATED threads are not alive.

P3Easy

Countdown with sleep

Print 3, 2, 1, Go! with a 100 ms pause between numbers, on the main thread.

Input
(none)
Expected output
3
2
1
Go!
Show solution and explanation
public class Main {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 3; i >= 1; i--) {
            System.out.println(i);
            Thread.sleep(100);
        }
        System.out.println("Go!");
    }
}
3 2 1 Go!

How it works: Thread.sleep is static: it always pauses the current thread (here, main). main declares throws InterruptedException.

P4Medium

Interrupt a sleeping thread

Start a thread that sleeps 10 seconds. Interrupt it at once and print the exception message it catches.

Input
(none)
Expected output
caught: sleep interrupted
Show solution and explanation
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(10_000);
            } catch (InterruptedException e) {
                System.out.println("caught: " + e.getMessage());
            }
        });
        t.start();
        t.interrupt();
        t.join();
    }
}
caught: sleep interrupted

How it works: interrupt() makes sleep throw InterruptedException (message "sleep interrupted"), so the program ends in milliseconds, not 10 seconds.

P5Medium

The flag is cleared

Interrupt the current thread, then call sleep. Print the interrupted flag after catching the exception.

Input
(none)
Expected output
flag set: true
sleep threw at once
flag now: false
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        Thread.currentThread().interrupt();
        System.out.println("flag set: " + Thread.currentThread().isInterrupted());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("sleep threw at once");
        }
        System.out.println("flag now: " + Thread.currentThread().isInterrupted());
    }
}
flag set: true sleep threw at once flag now: false

How it works: when sleep throws InterruptedException it also clears the flag. If you can't handle the interrupt, restore it with Thread.currentThread().interrupt() so callers still see it.

P6Medium

Join in a chain

Thread B must print only after thread A has finished. Make B call a.join() before printing. main joins B.

Input
(none)
Expected output
A: data downloaded
B: data processed
main: all done
Show solution and explanation
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(() -> System.out.println("A: data downloaded"));
        Thread b = new Thread(() -> {
            try {
                a.join();
            } catch (InterruptedException e) {
                return;
            }
            System.out.println("B: data processed");
        });
        a.start();
        b.start();
        b.join();
        System.out.println("main: all done");
    }
}
A: data downloaded B: data processed main: all done

How it works: any thread can join any other. B is WAITING inside a.join() until A terminates, so B's line always comes second, whichever thread the scheduler runs first. (Careful: if B called a.join() before A was started, join would return at once, because a NEW thread isn't alive.)

P7Business problem

OTP expiry timer

An OTP is valid for 3 ticks. A timer thread prints the remaining ticks with 50 ms sleeps and then marks the OTP expired. main waits and prints the final status.

Input
OTP 482913, 3 ticks
Expected output
OTP 482913 valid for 3 more tick(s)
OTP 482913 valid for 2 more tick(s)
OTP 482913 valid for 1 more tick(s)
OTP status: EXPIRED
Show solution and explanation
public class Main {
    static String status = "VALID";

    public static void main(String[] args) throws InterruptedException {
        Thread timer = new Thread(() -> {
            try {
                for (int left = 3; left > 0; left--) {
                    System.out.println("OTP 482913 valid for " + left + " more tick(s)");
                    Thread.sleep(50);
                }
                status = "EXPIRED";
            } catch (InterruptedException e) {
                status = "CANCELLED";
            }
        }, "otp-timer");
        timer.start();
        timer.join();
        System.out.println("OTP status: " + status);
    }
}
OTP 482913 valid for 3 more tick(s) OTP 482913 valid for 2 more tick(s) OTP 482913 valid for 1 more tick(s) OTP status: EXPIRED

How it works: the timer is mostly TIMED_WAITING. If the user verified the OTP early, main could interrupt() the timer and the status would become CANCELLED.

P8Business problem

Cancel a bulk SMS run

A marketing job sends 1,000 SMS, one per 500 ms. Operations cancels it right after start. The job must check for the interrupt, stop, and report how many were sent.

Input
1,000 messages, cancelled immediately
Expected output
campaign stopped by operator
messages sent: 1 of 1000
Show solution and explanation
public class Main {
    public static void main(String[] args) throws InterruptedException {
        int[] sent = {0};
        Thread job = new Thread(() -> {
            for (int i = 1; i <= 1000; i++) {
                sent[0]++;
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    System.out.println("campaign stopped by operator");
                    return;
                }
            }
        }, "sms-campaign");
        job.start();
        job.interrupt();
        job.join();
        System.out.println("messages sent: " + sent[0] + " of 1000");
    }
}
campaign stopped by operator messages sent: 1 of 1000

How it works: the job checks for cancellation at every sleep. A thread that never sleeps or checks isInterrupted() can't be cancelled politely; that's why long loops should check the flag.

P9Business problem

Pipeline with join timeouts

A nightly pipeline has a fast step (sleeps 10 ms) and a stuck step (sleeps 60 s). Give each join(200). Print which steps completed and cancel the stuck one.

Input
steps: export (fast), upload (stuck)
Expected output
export: OK
upload: TIMEOUT
upload cancelled
Show solution and explanation
public class Main {
    static Thread step(String name, long ms) {
        return new Thread(() -> {
            try {
                Thread.sleep(ms);
            } catch (InterruptedException e) {
                System.out.println(name + " cancelled");
            }
        }, name);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] steps = {step("export", 10), step("upload", 60_000)};
        for (Thread s : steps) s.start();
        for (Thread s : steps) {
            s.join(200);
            if (s.isAlive()) {
                System.out.println(s.getName() + ": TIMEOUT");
                s.interrupt();
                s.join();
            } else {
                System.out.println(s.getName() + ": OK");
            }
        }
    }
}
export: OK upload: TIMEOUT upload cancelled

How it works: join(ms) never blocks longer than the limit. Combining it with isAlive() and interrupt() is the classic timeout pattern for raw threads (executors give you Future.get(timeout), Lesson 12.6).

Real-world scenario questions (asked in company interviews)

These test whether you can reason about states in a running system, not just recite them.

Production · thread dumpJunior

150 threads BLOCKED on one line

During a sale, the order service becomes slow. A thread dump shows 150 request threads BLOCKED waiting for the same lock in InventoryService.reserve(), and one thread RUNNABLE inside it, calling a slow pricing API. What's going on?

The interviewer is checking: reading states to find a bottleneck.

Show a strong answer
  • The method is synchronized, and the thread holding the lock is doing slow I/O inside it, so every other request queues up as BLOCKED.
  • Move the pricing call outside the locked section, and lock only the few lines that update shared stock, or use finer-grained locks.

Avoid: adding more threads; they would just join the BLOCKED queue.

Code review · interruptsJunior

The Cancel button that doesn't cancel

An export thread's code is try { Thread.sleep(100); } catch (InterruptedException e) { } inside a loop of 10,000 rows. Pressing Cancel calls interrupt(), but the export keeps going. Why?

The interviewer is checking: swallowed interrupts.

Show a strong answer
  • The empty catch swallows the interrupt, and throwing cleared the flag, so the loop just continues.
  • In the catch, stop the loop (return or break), or restore the flag with Thread.currentThread().interrupt() and check it in the loop condition.

Avoid: “use Thread.stop()”. It's deprecated and unsafe.

Integration · timeoutsJunior

A partner API that sometimes never answers

A travel site's search calls three airline APIs on three threads and then calls join() on each. Once a week one airline's API hangs and the search page never loads. Fix it.

The interviewer is checking: timed waits and fallbacks.

Show a strong answer
  • Plain join() waits forever. Use a deadline: join(remainingMs) per thread (or Future.get(timeout)).
  • After the deadline, interrupt the slow thread, show results from the airlines that answered, and log the timeout.
  • Also set network timeouts in the HTTP client itself.

Avoid: waiting forever for anything outside your system.

Performance · CPUMid

A “waiting” thread using 100% CPU

A worker waits for a flag with while (!ready) { }. The server shows one core at 100%, and the thread dump says the worker is RUNNABLE. Explain and fix.

The interviewer is checking: busy-waiting vs blocking states.

Show a strong answer
  • A busy loop never leaves RUNNABLE, so it burns a whole core doing nothing useful. It may also never see the change if ready isn't volatile (Lesson 12.4).
  • Use a blocking mechanism: wait/notify (Lesson 12.5), a CountDownLatch, a BlockingQueue, or join(), so the thread is WAITING and uses no CPU.

Avoid: adding Thread.sleep(1) in the loop as the final fix; it still wakes up constantly and adds delay.