Course contents

Daemon threads and priorities

When the JVM really exits, why daemon threads die without running finally, which threads are daemons by default, and what thread priorities actually do on Linux, Windows and macOS.

PremiumMust know for interviewsLesson20 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: staff and cleaners
  2. The exit rule, precisely
  3. Under the hood: watch a daemon get cut off
  4. The rules of setDaemon, and who is a daemon by default
  5. When a daemon is right, and when it never is
  6. Thread priorities: hints, not orders
  7. Real world
  8. Advanced: edge cases, pitfalls and what to use instead
  9. How to say it in the interview

🌙 What are daemon threads and thread priorities?

A daemon thread is a background thread that doesn't keep the JVM alive. The JVM starts shutting down as soon as the last non-daemon (user) thread finishes, and any daemon still running is stopped mid-instruction, with no finally and no cleanup. A thread priority is a number from 1 to 10 that you pass to the OS scheduler as a hint, and on a default Linux JVM that hint is ignored entirely.

For this lesson, we'll pin down exactly when a JVM exits, what happens to the threads still running at that moment, and what the priority number really does on Linux, Windows and macOS. Interviewers like this pair because both look like trivia (setDaemon(true), setPriority(10)) and both hide production bugs: lost log lines, programs that never exit, and code that "works on my machine" because of scheduling luck.

Below the line (out of scope)

The 30-second answer

Every platform thread is either a user (non-daemon) thread or a daemon thread. The JVM begins its shutdown sequence when the last non-daemon thread terminates, when someone calls System.exit/Runtime.exit, or when the OS sends a signal like SIGTERM. Shutdown hooks run, then the JVM terminates and daemon threads are abandoned: their finally blocks never run. You must call setDaemon(true) before start(), otherwise you get IllegalThreadStateException; a new thread inherits daemon status and priority from the thread that creates it. Priorities run from 1 to 10 (default 5) and are only hints: HotSpot ignores them on Linux by default. Virtual threads are always daemon and always priority 5.

What you’ll learn

  • State the JVM exit rule precisely: the last non-daemon thread ends, System.exit/Runtime.exit is called, or a signal arrives
  • Explain the shutdown sequence: hooks run concurrently, then the JVM terminates and daemon threads stop with **no finally**
  • Use setDaemon(true) before start() (else IllegalThreadStateException) and predict inherited daemon status and priority
  • Know the defaults that bite: newFixedThreadPool threads are user threads, while commonPool(), virtual threads and Timer(true) are daemons
  • Pick a safe pattern for background work: user thread + orderly shutdown, or daemon + flushing shutdown hook
  • Describe priorities 1 to 10 (NORM_PRIORITY 5), the thread-group cap and why they are hints: ignored on Linux by default, five levels on Windows
  • Diagnose "my program never exits" with jcmd <pid> Thread.print, and replace priorities with bulkheads, priority queues and rate limits

Interview radar

Asked very often: What is a daemon thread? Will its finally block run? Core follow-up: Why doesn't my program exit after main returns? Do thread priorities guarantee order? Production angle: Why did the last few seconds of logs or Kafka messages vanish when the service restarted?

01Picture it first: staff and cleaners

Picture an office building. The building stays open as long as at least one employee is inside. The night cleaners also work there, but nobody keeps the lights on for them: when the last employee badges out, security does a final round, then locks the doors with the cleaners still inside, mop in hand.

Employees are user threads. Cleaners are daemon threads. Security's final round is the shutdown hooks. And the mop left in the bucket is the finally block that never ran. The figure turns that story into the decision the JVM makes.

Fig. 1

The JVM exit decision: it keeps running while any user thread is alive, then runs shutdown hooks and halts, abandoning daemon threads without running their finally blocks

The JVM exit decision: it keeps running while any user thread is alive, then runs shutdown hooks and halts, abandoning daemon threads without running their finally blocks

02The exit rule, precisely

JLS §12.8 (Java 21 edition) and the "Shutdown Sequence" section of the Runtime Javadoc give three triggers that start the shutdown sequence:

  1. The number of live non-daemon threads drops to zero for the first time. Threads you created but never started don't count.
  2. Some thread calls System.exit(status) or Runtime.getRuntime().exit(status).
  3. An external event: Ctrl+C (SIGINT), SIGTERM from kill or Kubernetes, user logoff.

Once it starts, the JVM starts every registered shutdown hook, in no particular order, and they run concurrently with whatever threads are still alive, daemon or not. When the last hook finishes, the JVM terminates. The Java 21 Runtime Javadoc spells out what "terminates" means: every thread stops executing Java code immediately, current methods complete neither normally nor abruptly, finally clauses don't run, and try-with-resources doesn't close anything.

Two escape hatches skip the hooks entirely. Runtime.halt(status) terminates without starting the shutdown sequence, and SIGKILL (kill -9, or Kubernetes after the grace period) kills the process before the JVM can do anything. Old JDKs could also run finalizers on exit with runFinalizersOnExit, but that was removed in JDK 11, so in Java 21 shutdown hooks are your only exit-time callback.

03Under the hood: watch a daemon get cut off

Try it yourself first

Before reading on, predict the output. main returns immediately, a user thread finishes after 500 ms, and a daemon flushes every 200 ms inside try/finally. Does finally: closing file print? Does the JVM exit when main returns, or later?

Here's the smallest program I use to show all three behaviours at once:

public class DaemonAbandoned {

    static long t0 = System.currentTimeMillis();

    static void log(String msg) {
        System.out.printf("%4d ms  %-14s %s%n", System.currentTimeMillis() - t0,
                Thread.currentThread().getName(), msg);
    }

    public static void main(String[] args) throws InterruptedException {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> log("shutdown hook runs"), "hook"));

        Thread flusher = new Thread(() -> {
            try {
                for (int batch = 1; ; batch++) {
                    log("flushing batch " + batch);
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                log("interrupted");
            } finally {
                log("finally: closing file");   // never printed
            }
        }, "daemon-flusher");
        flusher.setDaemon(true);

        Thread report = new Thread(() -> {
            try {
                Thread.sleep(500);
            } catch (InterruptedException ignored) {
            }
            log("report written, exiting");
        }, "user-report");

        flusher.start();
        report.start();
        log("main returns (daemon? " + Thread.currentThread().isDaemon() + ")");
    }
}

java DaemonAbandoned.java on JDK 21 printed this (the first two lines can swap):

   0 ms  daemon-flusher flushing batch 1
   1 ms  main           main returns (daemon? false)
 210 ms  daemon-flusher flushing batch 2
 415 ms  daemon-flusher flushing batch 3
 501 ms  user-report    report written, exiting
 501 ms  hook           shutdown hook runs

Notice four things:

  1. main ending didn't end the program. user-report is a user thread, so the JVM stayed up another 500 ms for it.
  2. The daemon kept working the whole time. Daemon threads aren't second-class while the JVM is alive; they get CPU like anyone else.
  3. The hook ran when the last user thread ended, at 501 ms, not when main returned.
  4. No interrupted, no finally. The daemon was asleep inside batch 3 when the JVM terminated. It wasn't interrupted, didn't get an exception and never reached finally. If that block had closed a file, the file would be left half-written.

Here is the same run as a timeline, with the cut-off moment marked.

Fig. 2

Timeline of the demo: main ends at 1 ms, the user thread ends at 501 ms, the shutdown hook runs, and the daemon flusher is cut off mid-sleep without reaching its finally block

Timeline of the demo: main ends at 1 ms, the user thread ends at 501 ms, the shutdown hook runs, and the daemon flusher is cut off mid-sleep without reaching its finally block

04The rules of setDaemon, and who is a daemon by default

Try it yourself first

Predict each line: what happens if you call setDaemon(true) on a running thread? Is a thread created by a daemon thread a daemon? Are ForkJoinPool.commonPool() workers daemons? What about threads from Executors.defaultThreadFactory()?

import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;

public class DaemonRules {

    public static void main(String[] args) throws Exception {
        Thread main = Thread.currentThread();
        System.out.println("main: daemon=" + main.isDaemon() + ", priority=" + main.getPriority());
        attempt("setDaemon on a started thread", () -> main.setDaemon(true));

        Thread parent = new Thread(() -> {                 // a daemon creates a child
            Thread child = new Thread(() -> { });
            System.out.println("child of daemon: daemon=" + child.isDaemon() + ", priority=" + child.getPriority());
        });
        parent.setDaemon(true);
        parent.setPriority(Thread.MAX_PRIORITY);
        parent.start();
        parent.join();

        Runnable noop = () -> { };
        System.out.println("ofPlatform().daemon(): " + Thread.ofPlatform().daemon().unstarted(noop).isDaemon());
        System.out.println("defaultThreadFactory: " + Executors.defaultThreadFactory().newThread(noop).isDaemon());
        ForkJoinPool.commonPool().submit(() ->
                System.out.println("commonPool worker: " + Thread.currentThread().isDaemon())).get();

        Thread virtual = Thread.ofVirtual().unstarted(noop);
        virtual.setPriority(Thread.MAX_PRIORITY);
        System.out.println("virtual: daemon=" + virtual.isDaemon() + ", priority=" + virtual.getPriority());
        attempt("virtual.setDaemon(false)", () -> virtual.setDaemon(false));
        attempt("setPriority(11)", () -> new Thread(noop).setPriority(11));

        ThreadGroup capped = new ThreadGroup("capped");
        capped.setMaxPriority(4);
        Thread inGroup = new Thread(capped, noop);
        inGroup.setPriority(Thread.MAX_PRIORITY);
        System.out.println("group max 4, setPriority(10) -> " + inGroup.getPriority());
    }

    static void attempt(String label, Runnable action) {
        try {
            action.run();
            System.out.println(label + " -> ok");
        } catch (RuntimeException e) {
            System.out.println(label + " -> " + e);
        }
    }
}

Output, identical on JDK 21 and JDK 26:

main: daemon=false, priority=5
setDaemon on a started thread -> java.lang.IllegalThreadStateException
child of daemon: daemon=true, priority=10
ofPlatform().daemon(): true
defaultThreadFactory: false
commonPool worker: true
virtual: daemon=true, priority=5
virtual.setDaemon(false) -> java.lang.IllegalArgumentException: 'false' not legal for virtual threads
setPriority(11) -> java.lang.IllegalArgumentException
group max 4, setPriority(10) -> 4

Daemon status is fixed at start(). On any live thread (here main itself), setDaemon throws IllegalThreadStateException, because the JVM has already counted it as user or daemon. Inheritance is the rule that surprises people: a thread created from inside a daemon is a daemon too, with the parent's priority, unless you say otherwise. Here's who is what by default:

Thread Daemon? Why
main No The launcher creates it as the one user thread
new Thread(...) Same as the creating thread Inherited; from main that means user
Thread.ofPlatform().daemon().start(r) Yes Builder option, final in Java 21 (JEP 444)
Executors.defaultThreadFactory() (used by newFixedThreadPool etc.) No, forced It calls setDaemon(false) and sets priority 5 on every thread
ForkJoinPool workers, including commonPool() Yes Specified in the ForkJoinPool Javadoc
Virtual threads Always setDaemon(false) throws IllegalArgumentException
new java.util.Timer(true) Yes The boolean is the timer thread's daemon flag; new Timer() is a user thread
JVM helpers: Reference Handler, Finalizer, Signal Dispatcher, Common-Cleaner Yes They serve your threads and must never block exit. GC and JIT threads are internal VM threads, invisible to Thread.getAllStackTraces()

Two rows in that table cause most real bugs. The fixed pool's threads are user threads, so forgetting shutdown() keeps the JVM alive forever. The common pool's threads are daemon threads, so work passed to CompletableFuture.runAsync without an executor can be silently dropped when main ends. We'll see both in a moment.

05When a daemon is right, and when it never is

Daemons fit work whose loss at exit costs nothing: evicting expired cache entries, sampling metrics that will be sampled again, sending heartbeats, watching a config file. They never fit work that must finish: writing a file, committing a transaction, acknowledging a message, sending the last batch of logs. A daemon gets no warning and no finally.

When background work also has a "last flush" requirement, I pick one of two patterns:

  1. Non-daemon worker plus orderly shutdown. The thread keeps the JVM alive, so you must stop it yourself: shutdown() and awaitTermination(), or try-with-resources on an ExecutorService (its close() waits for tasks, since Java 19).
  2. Daemon worker plus a shutdown hook that flushes. The daemon does the periodic work; the hook, which the JVM always waits for, does the final flush.

This program uses both:

Premium lesson

Keep reading with the Java Pass

The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.

  • Thread priorities: hints, not orders
  • Real world
  • Advanced: edge cases, pitfalls and what to use instead
  • How to say it in the interview

From ₹299 · one-time payment · No auto-renewal

Practise this topicThread basics: interview questions & scenarios
Further reading (14)