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.
On this page
- Picture it first: staff and cleaners
- The exit rule, precisely
- Under the hood: watch a daemon get cut off
- The rules of setDaemon, and who is a daemon by default
- When a daemon is right, and when it never is
- Thread priorities: hints, not orders
- Real world
- Advanced: edge cases, pitfalls and what to use instead
- 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)
- Creating and starting threads: Creating threads: Thread, Runnable and Callable
- States like RUNNABLE and TIMED_WAITING: Thread lifecycle and states
- Stopping a thread politely with
interrupt(): Interrupts and cancellation - Pool sizing and
ExecutorServicein depth: the "ExecutorService and thread pools" lesson (coming soon)
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.exitis 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)beforestart()(elseIllegalThreadStateException) and predict inherited daemon status and priority - Know the defaults that bite:
newFixedThreadPoolthreads are user threads, whilecommonPool(), virtual threads andTimer(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_PRIORITY5), 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.
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:
- The number of live non-daemon threads drops to zero for the first time. Threads you created but never started don't count.
- Some thread calls
System.exit(status)orRuntime.getRuntime().exit(status). - An external event: Ctrl+C (SIGINT), SIGTERM from
killor 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:
mainending didn't end the program.user-reportis a user thread, so the JVM stayed up another 500 ms for it.- The daemon kept working the whole time. Daemon threads aren't second-class while the JVM is alive; they get CPU like anyone else.
- The hook ran when the last user thread ended, at 501 ms, not when
mainreturned. - No
interrupted, nofinally. The daemon was asleep inside batch 3 when the JVM terminated. It wasn't interrupted, didn't get an exception and never reachedfinally. 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.
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:
- Non-daemon worker plus orderly shutdown. The thread keeps the JVM alive, so you must stop it yourself:
shutdown()andawaitTermination(), or try-with-resources on anExecutorService(itsclose()waits for tasks, since Java 19). - 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
Further reading (14)
- Thread (Java SE 21 API): setDaemon, isDaemon, setPriority, inheritance, virtual threads always daemon with fixed priority
- Runtime (Java SE 21 API): Shutdown Sequence and JVM Termination: three triggers, hooks run concurrently, finally clauses not executed at termination, exit blocks indefinitely, halt skips hooks
- Java Language Specification (Java SE 21) §12.8: Program Exit: non-daemon threads, daemon threads and shutdown hooks
- Executors (Java SE 21 API): defaultThreadFactory: each new thread is a non-daemon thread with NORM_PRIORITY
- ForkJoinPool (Java SE 21 API): all worker threads, including the common pool's, are daemon threads
- HotSpot globals.hpp (JDK 21u): ThreadPriorityPolicy, JavaPriorityN_To_OSPriority: policy 0 ignores priorities on Linux; policy 1 needs root or a capability on Linux/BSD/macOS
- HotSpot os_linux.cpp (JDK 21u): java_to_os_priority and set_native_priority: niceness 4 to -5; no-op unless ThreadPriorityPolicy=1; macOS table (Mach 27 to 36) is in os_bsd.cpp
- HotSpot os_windows.cpp (JDK 21u): Windows priority tables: ten Java priorities compressed onto LOWEST..HIGHEST; policy 1 adds TIME_CRITICAL
- Linux kernel sched/core.c: sched_prio_to_weight: nice -5 = weight 3121, nice 4 = weight 423
- Logback manual: AsyncAppender: must stop the LoggerContext to flush; maxFlushTime 1000 ms default; shutdown hook advice; worker thread is a daemon in AsyncAppenderBase
- Log4j 2 manual: log4j2.shutdownHookEnabled: Log4j installs a shutdown hook that stops logger contexts
- KafkaProducer (Apache Kafka API): buffered records and a background I/O thread; always close(); the sender thread is created as a daemon
- OkHttp OkHttpClient.kt: Shutdown Isn't Necessary: idle threads are released automatically; Dispatcher.kt builds non-daemon threads with a 60 s keep-alive
- Kubernetes: Pod lifecycle, termination of Pods: SIGTERM, then SIGKILL after terminationGracePeriodSeconds (30 s default)