Avoiding Thread Hunger Locks When Submitting Dependent Tasks to a Bounded Thread Pool
The article explains how converting a serial RPC call layer to an asynchronous parallel model can cause thread pool exhaustion and hunger lock when tasks depend on each other, demonstrates the issue with Java code, and provides practical strategies such as using separate pools or CompletableFuture to avoid the problem.
To improve system throughput and shorten page response time, a department refactored its aggregated service layer (the B‑side/C‑side API layer) from serial RPC calls to an asynchronous parallel mode. Shortly after deployment, the system raised numerous thread‑pool resource‑exhaustion alarms.
Exception log:
Exception in thread "main" java.util.concurrent.ExecutionException:
java.util.concurrent.RejectedExecutionException:
Task java.util.concurrent.FutureTask@42936575[Not completed, task = xxxxx] rejected from java.util.concurrent.ThreadPoolExecutor@33f18ac[Running, pool size = X, active threads = X, queued tasks = N, completed tasks = M]The logs show that both the thread pool and its queue are exhausted. The root cause was not an insufficient pool size but business code that submitted tasks with mutual dependencies into the same pool.
Pitfall: Executing inter‑dependent tasks in a limited‑size thread pool leads to a hunger lock, where parent tasks occupy threads while waiting for child tasks that cannot be scheduled.
Problematic code (thread‑pool definition):
private static final ExecutorService poolExecutor = new ThreadPoolExecutor(2, 2,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(),
new ThreadPoolExecutor.AbortPolicy() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
log.error("rejectedExecution");
super.rejectedExecution(r, e);
}
}
);Pool size: 2, queue size: 1, rejection policy: AbortPolicy.
Problematic code (tasks with dependencies):
/**
* @author 认知科技技术团队
* 微信公众号:认知科技技术团队
*/
public static void main(String[] args) throws ExecutionException, InterruptedException {
Future<Object> future1 = poolExecutor.submit(() -> {
Future<Object> future = poolExecutor.submit(() -> null);
return future.get();
});
Future<Object> future2 = poolExecutor.submit(() -> {
Future<Object> future = poolExecutor.submit(() -> null);
return future.get();
});
future1.get();
future2.get();
poolExecutor.shutdown();
}Each parent task submits a child task to the same pool and waits for its result. Because the pool has only two threads, both threads become occupied by the parent tasks, leaving no free thread for the child tasks, which results in a hunger lock.
How to avoid the pitfall:
Do not use a small or fixed‑size thread pool for tasks that have inter‑dependencies.
Avoid using Future#get() without a timeout; it blocks indefinitely.
Consider CallerRunsPolicy as a rejection policy, but be aware it may affect web‑container threads.
Isolate dependent tasks into separate thread pools.
Use CompletableFuture with a custom thread pool to orchestrate dependent tasks.
Conclusion
Never execute inter‑dependent tasks in a bounded thread pool; doing so can cause a hunger lock and system failure. Isolate dependent tasks into different pools or employ CompletableFuture with a dedicated executor to safely compose such tasks.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Cognitive Technology Team
Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
