Java
ExecutorService
Thread Management
Daemon Threads
Concurrency

Turning an ExecutorService to daemon in Java

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

By default, Java's ExecutorService creates non-daemon (user) threads, which prevent the JVM from exiting even when the main method completes. To make the JVM exit when only executor threads remain, create the ExecutorService with a custom ThreadFactory that sets thread.setDaemon(true). Daemon threads are automatically terminated when all user threads finish. This is useful for background tasks like periodic cleanup, logging, and monitoring that should not keep the application alive.

The Problem

java
1import java.util.concurrent.*;
2
3public class Main {
4    public static void main(String[] args) {
5        ExecutorService executor = Executors.newFixedThreadPool(2);
6        executor.submit(() -> {
7            while (true) {
8                System.out.println("Background task running...");
9                Thread.sleep(1000);
10            }
11        });
12
13        System.out.println("Main method finished");
14        // JVM does NOT exit — the executor's non-daemon threads keep it alive
15    }
16}

Without daemon threads, the JVM stays running indefinitely because the executor's threads are non-daemon by default.

Custom ThreadFactory with Daemon Threads

java
1import java.util.concurrent.*;
2
3public class DaemonExecutorExample {
4    public static void main(String[] args) throws InterruptedException {
5        ExecutorService executor = Executors.newFixedThreadPool(2, r -> {
6            Thread t = new Thread(r);
7            t.setDaemon(true);
8            return t;
9        });
10
11        executor.submit(() -> {
12            while (true) {
13                System.out.println("Background task running...");
14                try { Thread.sleep(1000); } catch (InterruptedException e) { break; }
15            }
16        });
17
18        System.out.println("Main method finished");
19        Thread.sleep(3000);  // Wait 3 seconds to see output
20        // JVM exits after main thread finishes — daemon threads are killed
21    }
22}

The lambda r -> { Thread t = new Thread(r); t.setDaemon(true); return t; } is a concise ThreadFactory implementation.

Reusable DaemonThreadFactory

java
1import java.util.concurrent.ThreadFactory;
2import java.util.concurrent.atomic.AtomicInteger;
3
4public class DaemonThreadFactory implements ThreadFactory {
5    private final AtomicInteger threadNumber = new AtomicInteger(1);
6    private final String namePrefix;
7
8    public DaemonThreadFactory(String namePrefix) {
9        this.namePrefix = namePrefix;
10    }
11
12    @Override
13    public Thread newThread(Runnable r) {
14        Thread t = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement());
15        t.setDaemon(true);
16        t.setPriority(Thread.NORM_PRIORITY);
17        return t;
18    }
19}
20
21// Usage
22ExecutorService executor = Executors.newFixedThreadPool(4, new DaemonThreadFactory("bg-worker"));

Named threads make debugging easier — stack traces show bg-worker-1 instead of Thread-47.

Using Guava's ThreadFactoryBuilder

java
1import com.google.common.util.concurrent.ThreadFactoryBuilder;
2
3ExecutorService executor = Executors.newFixedThreadPool(4,
4    new ThreadFactoryBuilder()
5        .setDaemon(true)
6        .setNameFormat("background-pool-%d")
7        .setUncaughtExceptionHandler((t, e) ->
8            System.err.println("Thread " + t.getName() + " failed: " + e))
9        .build()
10);

ScheduledExecutorService with Daemon Threads

java
1ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, r -> {
2    Thread t = new Thread(r);
3    t.setDaemon(true);
4    t.setName("scheduler-daemon");
5    return t;
6});
7
8scheduler.scheduleAtFixedRate(
9    () -> System.out.println("Heartbeat: " + System.currentTimeMillis()),
10    0, 5, TimeUnit.SECONDS
11);
12
13// Scheduler stops when the JVM exits (no non-daemon threads left)

Java 21+ Virtual Threads

Java 21's virtual threads are daemon threads by default:

java
1ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
2// Virtual threads are always daemon — no ThreadFactory needed
3
4executor.submit(() -> {
5    System.out.println("Virtual thread (daemon by default): "
6        + Thread.currentThread().isDaemon());  // true
7});

Graceful Shutdown (Best Practice)

Even with daemon threads, prefer explicit shutdown for clean resource release:

java
1ExecutorService executor = Executors.newFixedThreadPool(2, r -> {
2    Thread t = new Thread(r);
3    t.setDaemon(true);
4    return t;
5});
6
7// Add a shutdown hook for graceful cleanup
8Runtime.getRuntime().addShutdownHook(new Thread(() -> {
9    executor.shutdown();
10    try {
11        if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
12            executor.shutdownNow();
13        }
14    } catch (InterruptedException e) {
15        executor.shutdownNow();
16    }
17}));

Common Pitfalls

  • Daemon threads being killed mid-task: The JVM kills daemon threads abruptly when all user threads finish. If a daemon thread is writing to a file or database, data may be lost or corrupted. Use shutdown hooks or executor.shutdown() for tasks that must complete.
  • Not calling executor.shutdown() on non-daemon executors: If you decide against daemon threads, you must call shutdown() explicitly. Otherwise, the JVM never exits because the executor's threads keep running.
  • Using daemon threads for critical tasks: Daemon threads are for fire-and-forget background work. Tasks that must run to completion (transaction processing, data persistence) should use non-daemon threads with explicit shutdown management.
  • Forgetting to name daemon threads: Unnamed threads appear as Thread-0, Thread-1 in stack traces, making debugging difficult. Always set a meaningful name in the ThreadFactory.
  • Mixing daemon and non-daemon threads in the same pool: An executor pool should be consistently daemon or non-daemon. Mixing causes confusing behavior where some tasks keep the JVM alive and others do not. Use separate executors for daemon and non-daemon workloads.

Summary

  • Create a ThreadFactory that calls thread.setDaemon(true) and pass it to Executors.newFixedThreadPool()
  • Daemon threads are killed when all user threads finish — the JVM does not wait for them
  • Use daemon threads for background tasks (monitoring, cleanup, logging) that should not block JVM exit
  • Use Guava's ThreadFactoryBuilder for a fluent API with naming, daemon, and exception handling
  • Java 21 virtual threads are daemon by default — no custom factory needed
  • Always prefer explicit shutdown() for clean resource release, even with daemon threads

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.