Backend Development 9 min read

Master Spring Boot 3 Virtual Threads: Prevent Daemon Issues & Keep Apps Alive

This guide explains how Spring Boot 3.2+ leverages JDK 21 virtual threads, demonstrates common pitfalls in non‑web applications—especially daemon thread termination—and shows how to enable keep‑alive configuration to ensure your tasks keep running, complete with code examples and performance insights.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Spring Boot 3 Virtual Threads: Prevent Daemon Issues & Keep Apps Alive

Environment: SpringBoot 3.2.5 + JDK 21

1. Introduction

Since SpringBoot 3.2.0‑M1, virtual threads are supported. Virtual threads, introduced in JDK 21, run without binding to OS threads, offering much lower cost and memory usage than platform threads. Using them can improve overall system performance.

2. Practical Example (Non‑Web Application)

When the application does not include spring-boot-starter-web (or sets .web(WebApplicationType.NONE) ), it runs in non‑web mode.

<code>public static void main(String[] args) {
    new SpringApplicationBuilder()
        .sources(SpringbootNonWebApplication.class)
        .web(WebApplicationType.NONE)
        .run(args);
}</code>

In a non‑web app, the embedded server is not started. If no non‑daemon threads are running, the program exits automatically.

2.1 Start a Scheduled Task

Define a scheduled task that runs every 3 seconds.

<code>@Component
public class TaskComponent {
    @Scheduled(fixedRate = 3000)
    public void task1() throws Exception {
        System.out.printf("Current thread: %s%n", Thread.currentThread());
        // TODO: execute task
        TimeUnit.SECONDS.sleep(1);
    }
}</code>

Enable the scheduler with @EnableScheduling .

2.2 Run Task with Virtual Threads

Enable virtual threads via Spring configuration:

<code>spring:
  threads:
    virtual:
      enabled: true</code>

After enabling, the task runs on a virtual thread, but the program terminates after a single log line because the virtual thread behaves like a daemon.

2.3 Daemon Thread Behavior

Simple thread example:

<code>Thread t = new Thread(() -> {
    try {
        System.out.println("start..." + System.currentTimeMillis());
        TimeUnit.SECONDS.sleep(5);
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println("over..." + System.currentTimeMillis());
});
t.start();</code>

Output shows the thread finishes, then the JVM exits after non‑daemon threads complete.

Marking the thread as daemon makes the JVM exit immediately with no output.

Virtual threads are always daemon threads; Thread.setDaemon(true/false) has no effect on them.

2.4 Keep‑Alive Configuration for Virtual Threads

Starting with SpringBoot 3.2.0‑RC1, the spring.main.keep-alive property can keep the application alive when only daemon (virtual) threads are running.

<code>spring:
  main:
    keep-alive: true</code>

With this setting, the application stays running instead of exiting.

2.5 Implementation Details

When spring.main.keep-alive=true , Spring Boot registers a listener that starts a non‑daemon thread which sleeps indefinitely, preventing the JVM from terminating.

<code>public class SpringApplication {
    public ConfigurableApplicationContext run(String... args) {
        // ...
        prepareContext(...);
        // ...
    }
    private void prepareContext(...) {
        // ...
        if (this.keepAlive) {
            context.addApplicationListener(new KeepAlive());
        }
        // ...
    }
}

private static final class KeepAlive implements ApplicationListener<ApplicationContextEvent> {
    public void onApplicationEvent(ApplicationContextEvent event) {
        if (event instanceof ContextRefreshedEvent) {
            startKeepAliveThread();
        } else if (event instanceof ContextClosedEvent) {
            stopKeepAliveThread();
        }
    }
    private void startKeepAliveThread() {
        Thread thread = new Thread(() -> {
            while (true) {
                try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException ignored) {}
            }
        });
        if (this.thread.compareAndSet(null, thread)) {
            thread.setDaemon(false);
            thread.setName("keep-alive");
            thread.start();
        }
    }
    private void stopKeepAliveThread() {
        Thread thread = this.thread.getAndSet(null);
        if (thread != null) {
            thread.interrupt();
        }
    }
}</code>

This simple mechanism ensures that virtual‑thread‑driven tasks can run without the application exiting prematurely.

JavaConcurrencySpring BootVirtual Threadskeepalive
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.