🔴 The Error You're Seeing

Confirm this matches your console output. If it does, you're in the right place.

ERROR LOG// DOC-DERIVED — canonical trace shape; line numbers vary by JDK build Exception in thread "pool-1-thread-3" java.lang.InterruptedException at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2056) at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2105) at java.base/java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:433) at com.devinhyderabad.pipeline.Worker.drainQueue(Worker.java:31) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)

⚡ Quick Fix Works 80% of the time

Catch the exception, restore the interrupt flag, and exit the loop cleanly — the interrupt means someone asked this worker to stop.

try { Job job = queue.take(); process(job); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // restore the signal return; // leave the drain loop — shutdown requested }

🧠 Why this Happens

Tap to expand the deep technical explanation

Blocking methods park the thread in the JVM scheduler. Thread.interrupt() flips an internal flag and wakes every parked thread; because a woken take() has no element to return, the contract forces it to throw InterruptedException instead of returning null or spinning. Throwing consumes the flag — that is why code that catches the exception but keeps working silently erases the cancellation request and the worker becomes unstoppable.

The HITEC City Parking Spot Analogy:

A night-shift guard asleep at the desk gets shaken awake and handed a memo saying shift over. He can either clock out (return) or pretend he was never woken (swallow the flag) — the second option leaves him guarding a door nobody needs guarded.

🔁 How to Reproduce Confirm this is your error

Start a consumer looping on a LinkedBlockingQueue.take(), call workerThread.interrupt() from main, and watch the trace surface from inside await/take. Same shape appears for put() when the buffer is full. (Canonical wording; not a lab capture.)

🛠️ Solutions (5 Ways to Fix)

Solution 1✓ Most common cause

Restore the flag and exit cleanly

👉 Use this when/if the interrupt arrives during planned shutdown and the worker owns its own loop.

Interruption is cooperative cancellation. Catching the exception without restoring the flag deletes the request; catching and immediately returning honors it. Restoring matters even when exiting because higher-level callers (executor worker threads) check the flag to mark the task cancelled.

public void run() { while (running.get()) { try { process(queue.take()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }
Solution 2

Shut down with a poison pill instead of interrupts

👉 Use this when/if you control both producer and consumer and want deterministic draining.

Pushing a sentinel SHUTDOWN element lets every queued job finish naturally and the consumer exit after the last real task. No interrupt, no partial work, and the queue drains in order — ideal for batch pipelines.

static final Job SHUTDOWN = new Job(); // producer side queue.put(SHUTDOWN); // consumer side Job job; while ((job = queue.take()) != SHUTDOWN) { process(job); }
Solution 3

Poll with a timeout and check the running flag

👉 Use this when/if you cannot guarantee anyone will ever interrupt the thread.

poll(500ms) bounds every park to half a second, so a forgotten shutdown still terminates within one tick. The loop rechecks the volatile flag each cycle, making liveness observable instead of dependent on a perfect caller.

while (running.get()) { Job job = queue.poll(500, TimeUnit.MILLISECONDS); if (job != null) { process(job); } }
Solution 4

Treat interruption as cancellation inside executor tasks

👉 Use this when/if the task runs inside an ExecutorService whose shutdownNow() interrupts workers.

shutdownNow() interrupts every live worker. A task that swallows InterruptedException keeps running after the pool reports termination, corrupting redeploy and scale-down logic. Convert the interrupt into an early, honest return so pool metrics match reality.

Future<?> f = pool.submit(() -> { try { return queue.take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new CancellationException("worker stopped"); } }); pool.shutdownNow(); f.get(); // CancellationException, not a hung task
Solution 5

DEV ONLY: swallow the exception and keep looping

👉 Use this never in production — only to observe the lost-cancellation bug in a scratch experiment.

An empty catch that continues take()ing looks harmless and compiles clean, but the thread can no longer be stopped: the interrupt was consumed, the loop parks again, and shutdown hangs forever. Shown here only so you recognize the anti-pattern in review.

// ANTI-PATTERN — do not ship try { process(queue.take()); } catch (InterruptedException e) { // ignored } // result: unstoppable worker, shutdownNow() appears to do nothing

📋 Version Notes

Java 8

Identical contract; AQS internals produce slightly different frame lines in the trace.

Java 11

No behavioral change; reportInterruptAfterWait remains the throwing frame.

Java 21

Virtual threads inherit the same rule — interrupting a virtual thread parked in take() throws here too, without pinning the carrier.

🛡️ How to Prevent This Next Time

Make interruption a documented part of every blocking loop: either restore the flag and exit, or convert to a poison-pill drain. Ban empty catch blocks for InterruptedException in code review, and exercise shutdown paths in CI with a test that interrupts workers mid-take.