Learn how to detect and fix thread leaks in Java applications using Java 21, thread dumps, ExecutorService, monitoring tools, and Spring Boot examples.
Introduction
Imagine running a restaurant where employees keep showing up for work but never leave.
At first, everything seems fine. But after a few hours, the kitchen becomes crowded, employees bump into each other, and service slows down. Eventually, the restaurant stops functioning.
That’s exactly what happens during a thread leak in Java applications.
Threads are essential workers inside a Java application. They process web requests, background jobs, database operations, and scheduled tasks. But when threads are created and never properly terminated, they continue consuming memory and CPU resources forever.
Over time, this causes:
- High CPU usage
- OutOfMemoryError issues
- Slow response times
- Application crashes
- Server instability
Understanding How to Detect and Fix Thread Leaks in Java Applications is one of the most important skills in modern Java programming.
In this guide, you’ll learn:
- What thread leaks are
- Why they happen
- How to detect them
- How to fix them properly
- Best practices for preventing them in production
If you want to learn Java concurrency and build stable applications, mastering thread leak detection is essential.
What Is a Thread Leak?
A thread leak happens when threads are continuously created but never cleaned up or terminated.
Instead of disappearing after completing work, leaked threads remain alive indefinitely.
Simple Real-World Analogy
| Real World | Java Application |
|---|---|
| Employees | Threads |
| Restaurant tasks | Application tasks |
| Employees never leaving | Threads never terminating |
| Crowded kitchen | Resource exhaustion |
| Restaurant slowdown | Application performance issues |
How Thread Leaks Happen
Common causes include:
- Forgetting to shut down thread pools
- Infinite loops inside threads
- Blocking operations
- Improper async handling
- Creating threads manually for every request
- Scheduled tasks running forever
Symptoms of Thread Leaks
1. Increasing Thread Count
Your application starts with 50 threads.
After several hours:
50 → 200 → 1000 → 5000 threads
That’s a warning sign.
2. High Memory Usage
Every thread consumes stack memory.
More threads = more memory consumption.
3. Slow Performance
Excessive threads increase:
- CPU context switching
- Garbage collection pressure
- Scheduler overhead
4. Application Crash
Eventually you may see:
java.lang.OutOfMemoryError: unable to create native thread
Core Concepts of Thread Leak Detection
1. Thread Lifecycle
A Java thread normally goes through:
NEW → RUNNABLE → WAITING → TERMINATED
Leaked threads never reach the TERMINATED state.
2. Thread Dumps
A thread dump is a snapshot of all running threads.
It helps identify:
- Stuck threads
- Deadlocks
- Infinite loops
- Excessive thread creation
3. ExecutorService
Instead of manually creating threads, Java recommends using ExecutorService.
Benefits:
- Reuses threads
- Controls thread limits
- Prevents uncontrolled thread growth
Common Causes of Thread Leaks
| Problem | Result |
|---|---|
Missing shutdown()
|
Thread pool never stops |
| Infinite loops | Threads run forever |
| Blocking network calls | Threads get stuck |
| Unbounded thread creation | Memory exhaustion |
| Forgotten scheduled tasks | Zombie threads |
Detecting Thread Leaks Using JDK Tools
Java provides built-in tools for thread analysis.
Useful Commands
View Running Java Processes
jps
Generate Thread Dump
jstack <PID>
Monitor Threads
jcmd <PID> Thread.print
Code Example 1 — Bad Example Causing a Thread Leak
This example demonstrates a common mistake: creating threads repeatedly without cleanup.
Project Setup
Maven pom.xml
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Leaky Thread Service
package com.example.threadleak.service;
import org.springframework.stereotype.Service;
@Service
public class LeakyThreadService {
public void startLeakyTask() {
// BAD PRACTICE:
// Creating a new thread for every request
Thread thread = new Thread(() -> {
while (true) {
try {
// Simulate background work
Thread.sleep(1000);
System.out.println(
"Running thread: "
+ Thread.currentThread().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
thread.start();
}
}
REST Controller
package com.example.threadleak.controller;
import com.example.threadleak.service.LeakyThreadService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LeakController {
private final LeakyThreadService service;
public LeakController(LeakyThreadService service) {
this.service = service;
}
@GetMapping("/leak")
public String createLeak() {
service.startLeakyTask();
return "Leaky thread started";
}
}
Run Application
mvn spring-boot:run
Trigger Leak Using curl
curl -X GET http://localhost:8080/leak
Run the command multiple times.
Sample Response
Leaky thread started
What Goes Wrong?
Each API request creates:
- A brand-new thread
- Infinite loop
- No shutdown mechanism
Result:
- Thread count continuously increases
- Memory usage spikes
- Application eventually crashes
This is a classic thread leak in Java applications.
Detect the Leak
Generate Thread Dump
jstack <PID>
Example Thread Dump Output
"Thread-25" #45 prio=5 os_prio=0 cpu=120ms elapsed=400s tid=0x000001 waiting
"Thread-26" #46 prio=5 os_prio=0 cpu=150ms elapsed=390s tid=0x000002 waiting
"Thread-27" #47 prio=5 os_prio=0 cpu=110ms elapsed=380s tid=0x000003 waiting
Notice how thread count keeps increasing.
Code Example 2 — Proper Fix Using ExecutorService
Now let’s solve the problem correctly using Java 21 best practices.
Fixed Service Using Thread Pool
package com.example.threadleak.service;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
public class SafeThreadService {
// Fixed-size thread pool
private final ExecutorService executorService =
Executors.newFixedThreadPool(5);
public void processTask() {
executorService.submit(() -> {
try {
System.out.println(
"Processing task using: "
+ Thread.currentThread().getName());
// Simulate work
Thread.sleep(3000);
System.out.println("Task completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// Proper cleanup during shutdown
@PreDestroy
public void shutdown() {
System.out.println("Shutting down thread pool");
executorService.shutdown();
}
}
REST Controller
package com.example.threadleak.controller;
import com.example.threadleak.service.SafeThreadService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SafeController {
private final SafeThreadService service;
public SafeController(SafeThreadService service) {
this.service = service;
}
@GetMapping("/safe-task")
public String processTask() {
service.processTask();
return "Task submitted successfully";
}
}
Run Application
mvn spring-boot:run
Test Endpoint
curl -X GET http://localhost:8080/safe-task
Sample Response
Task submitted successfully
Console Output
Processing task using: pool-1-thread-1
Task completed
Notice:
- Threads are reused
- Thread count remains stable
- No resource leak occurs
Modern Java 21 Alternative — Virtual Threads
Java 21 introduces Virtual Threads.
They are lightweight threads managed by the JVM.
Example
ExecutorService executor =
Executors.newVirtualThreadPerTaskExecutor();
Benefits:
- Millions of lightweight threads
- Reduced memory usage
- Better scalability
- Simpler concurrency model
Spring Boot Virtual Thread Support
Enable virtual threads:
spring.threads.virtual.enabled=true
This is one of the best modern solutions for preventing thread exhaustion.
Tools for Detecting Thread Leaks
| Tool | Purpose |
|---|---|
| jstack | Generate thread dumps |
| jcmd | JVM diagnostics |
| VisualVM | Monitor thread activity |
| JConsole | JVM monitoring |
| Spring Boot Actuator | Application metrics |
Best Practices for Preventing Thread Leaks
1. Always Shut Down ExecutorService
Bad:
Executors.newFixedThreadPool(10);
Good:
executorService.shutdown();
2. Avoid Manual Thread Creation
Prefer:
ExecutorService- Spring
@Async - Virtual threads
instead of:
new Thread()
3. Monitor Thread Counts Regularly
Use:
- Grafana
- Prometheus
- Spring Boot Actuator
to monitor production systems.
4. Use Timeouts for Blocking Operations
Never allow threads to wait forever.
Bad:
socket.read();
Good:
socket.setSoTimeout(5000);
5. Prefer Virtual Threads in Java 21
Virtual threads dramatically reduce the risk of thread exhaustion.
Common Mistakes Beginners Make
| Mistake | Problem |
|---|---|
| Creating threads per request | Resource exhaustion |
| Forgetting shutdown | Zombie threads |
| Infinite loops | CPU spikes |
| Blocking threads indefinitely | Application freeze |
| Ignoring monitoring | Hidden leaks |
Learn More
Conclusion
Understanding How to Detect and Fix Thread Leaks in Java Applications is critical for building reliable backend systems.
Here’s what you learned:
- What thread leaks are
- Why they happen
- How to detect them using thread dumps
- How to fix them using
ExecutorService - Why Java 21 virtual threads are important
Thread leaks are dangerous because they slowly destroy application performance over time.
But with proper monitoring, thread pools, and modern Java concurrency tools, you can build scalable and stable systems confidently.
If you’re serious about Java programming and want to learn Java concurrency deeply, thread management is a must-have skill.
Call to Action
Have you ever faced thread leaks in production systems?
Share your experience, ask questions, or discuss Java concurrency challenges in the comments.
Top comments (0)