Definition
- newFixedThreadPool(n) – internally creates
new ThreadPoolExecutor(n, n, 0, MILLISECONDS, new LinkedBlockingQueue<>()). The queue capacity isInteger.MAX_VALUE, effectively unbounded. - No rejection – the queue never fills up, so the rejection handler never fires. Tasks accumulate silently.
- OOM – each queued Runnable and its captured state occupy heap memory. Under sustained overload, millions of tasks queue up and the JVM runs out of heap.
- newCachedThreadPool() –
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, SECONDS, new SynchronousQueue<>()). Creates a new thread for every task that cannot immediately find a free thread. Slow tasks create unbounded threads. - Fix – use
ThreadPoolExecutordirectly with a boundedLinkedBlockingQueue(capacity)and a rejection handler. - Code quality – Alibaba Java Coding Guidelines and SonarQube flag
Executorsfactory methods as violations for this reason.
newFixedThreadPool is like hiring exactly 10 tellers at National Bank of India but giving them an infinite waiting line. During a rush, 100,000 customers queue up. The first customer is served in 1 minute. The last waits 5 hours. Eventually the building collapses under the weight of the line (OOM). Nobody was ever turned away, so the line kept growing. Fix: limit the waiting area to 500 seats. The 501st customer is told "come back later" and the building survives.
Understanding the Concepts
Why is newFixedThreadPool dangerous?
The Executors convenience factory methods hide unsafe defaults. newFixedThreadPool creates a ThreadPoolExecutor with an unbounded queue that can grow to Integer.MAX_VALUE entries.
// What newFixedThreadPool actually creates:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>() // UNBOUNDED!
);
}
What about other Executors factory methods?
All factory methods have similar problems:
| Factory method | Danger |
|---|---|
| newFixedThreadPool(n) | Unbounded queue: OOM |
| newSingleThreadExecutor() | Unbounded queue: OOM |
| newCachedThreadPool() | Unbounded threads: thread explosion |
| newScheduledThreadPool(n) | Unbounded DelayedWorkQueue: OOM |
What is the safe alternative?
package com.bank.concurrency;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class SafePoolDemo {
private static final AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
10, 20, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500), // BOUNDED
r -> new Thread(r, "nbi-worker-" + counter.incrementAndGet()),
new ThreadPoolExecutor.CallerRunsPolicy() // back-pressure
);
// Queue full + 20 workers: CallerRunsPolicy slows producer
// Bounded memory, predictable behavior, no OOM
}
}
What are common interview follow-up questions?
- Is newFixedThreadPool ever safe? Only if the producer rate is always lower than the consumer rate and you can guarantee the queue will never grow. In practice, this is impossible to guarantee in production.
- Why does SonarQube flag this? Because the unbounded queue is an OOM risk that is invisible until production load triggers it.
What are common mistakes?
A common mistake is not understanding the concept fully before using it in production banking applications.
Wrong approach:
package com.bank.service;
public class TransferService {
public void transfer(String fromAccount, String toAccount, double amount) {
// WRONG: using double for money
double balance = getBalance(fromAccount);
balance = balance - amount;
}
private double getBalance(String id) { return 10000.0; }
}
Correct approach:
package com.bank.service;
import java.math.BigDecimal;
public class TransferService {
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
// CORRECT: using BigDecimal for money
BigDecimal balance = getBalance(fromAccount);
balance = balance.subtract(amount);
}
private BigDecimal getBalance(String id) { return new BigDecimal("10000.00"); }
}
Always use BigDecimal for monetary calculations in banking systems to avoid floating-point precision errors.
What should a senior engineer know?
A senior engineer working on banking systems should understand the production implications of this concept.
- Performance impact. Measure with JMH before making assumptions about which approach is faster.
- Thread safety. In a multi-threaded banking service processing concurrent transactions, always consider whether the approach is thread-safe.
- Memory footprint. Use JFR and VisualVM to measure actual memory usage under realistic banking workloads.
- Monitoring. Set up JMX metrics and alerts for production banking applications.
package com.bank.monitoring;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
public class MemoryMonitor {
public static void printMemoryUsage() {
MemoryMXBean mem = ManagementFactory.getMemoryMXBean();
System.out.println("Heap: " + mem.getHeapMemoryUsage());
System.out.println("Non-Heap: " + mem.getNonHeapMemoryUsage());
}
}
What are the edge cases?
Edge cases can cause unexpected behavior in banking applications. Here are some to watch for.
package com.bank.edge;
public class EdgeCaseDemo {
public static void main(String[] args) {
// Edge case 1: null handling
String accountId = null;
try {
System.out.println(accountId.length());
} catch (NullPointerException e) {
System.out.println("Null account ID");
}
// Edge case 2: empty vs null
String emptyId = "";
System.out.println(emptyId.isEmpty()); // true
System.out.println(emptyId == null); // false
// Edge case 3: boundary values
java.math.BigDecimal zero = java.math.BigDecimal.ZERO;
java.math.BigDecimal negativeBalance = new java.math.BigDecimal("-0.01");
System.out.println(zero.compareTo(negativeBalance) > 0); // true
}
}
Null account ID
true
false
true
Always test with null values, empty strings, boundary values, and negative amounts in banking applications.
Visual Overview
Executors.newFixedThreadPool: Hidden Danger ════════════════════════════════════════════ newFixedThreadPool(10): ┌─────────────────────────────────────────┐ │ 10 workers │ LinkedBlockingQueue(MAX_INT)│ │ │ 1K... 10K... 100K... 1M... │ → OOM! │ │ Rejection: NEVER fires! │ └─────────────────────────────────────────┘ Safe alternative: ┌─────────────────────────────────────────┐ │ 10-20 workers │ LinkedBlockingQueue(500) │ │ │ Task 501: REJECTED │ │ │ CallerRunsPolicy fires │ └─────────────────────────────────────────┘
Key Takeaway
Executors.newFixedThreadPool() is dangerous in production because it uses an unbounded LinkedBlockingQueue that can grow to exhaust the heap. Under sustained overload, tasks pile up indefinitely with no rejection, no warning, and no metric. The JVM eventually dies with OutOfMemoryError. newCachedThreadPool() is equally dangerous with unbounded thread creation.
The fix is straightforward: use ThreadPoolExecutor directly with a bounded LinkedBlockingQueue and a rejection policy. CallerRunsPolicy provides natural back-pressure by making the submitting thread execute the rejected task, slowing down submission when the pool is saturated. Alibaba Java Coding Guidelines and SonarQube both flag Executors factory methods as violations.
At National Bank of India, a statement generation service using newFixedThreadPool(5) queued 3 million tasks during a batch run, consuming 6 GB of heap before crashing. After switching to ThreadPoolExecutor with a bounded queue of 10,000 and CallerRunsPolicy, the same batch completed in 6 hours with zero OOM incidents.
Remember one sentence: never use Executors factory methods in production because their unbounded queues or threads are OOM time bombs; always use ThreadPoolExecutor with bounded queues.
---