🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
// 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)
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;
}
}
}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);
}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);
}
}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 taskDEV 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
Identical contract; AQS internals produce slightly different frame lines in the trace.
No behavioral change; reportInterruptAfterWait remains the throwing frame.
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.