DEV Community

realNameHidden
realNameHidden

Posted on

How to Detect and Fix Thread Leaks in Java Applications

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Core Concepts of Thread Leak Detection

1. Thread Lifecycle

A Java thread normally goes through:

NEW → RUNNABLE → WAITING → TERMINATED
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Generate Thread Dump

jstack <PID>
Enter fullscreen mode Exit fullscreen mode

Monitor Threads

jcmd <PID> Thread.print
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

Run Application

mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode

Trigger Leak Using curl

curl -X GET http://localhost:8080/leak
Enter fullscreen mode Exit fullscreen mode

Run the command multiple times.

Sample Response

Leaky thread started
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

Run Application

mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode

Test Endpoint

curl -X GET http://localhost:8080/safe-task
Enter fullscreen mode Exit fullscreen mode

Sample Response

Task submitted successfully
Enter fullscreen mode Exit fullscreen mode

Console Output

Processing task using: pool-1-thread-1
Task completed
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Good:

executorService.shutdown();
Enter fullscreen mode Exit fullscreen mode

2. Avoid Manual Thread Creation

Prefer:

  • ExecutorService
  • Spring @Async
  • Virtual threads

instead of:

new Thread()
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Good:

socket.setSoTimeout(5000);
Enter fullscreen mode Exit fullscreen mode

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)