Safely Shut Down a Java ThreadPool with Daemon Threads for Asynchronous Tasks

This article explains how to create a fixed-size Java thread pool, attach a daemon thread that monitors the main thread, and use Phaser for synchronization, providing complete Groovy and Java examples with detailed code and console output.

FunTester
FunTester
FunTester
Safely Shut Down a Java ThreadPool with Daemon Threads for Asynchronous Tasks

In the previous article the author introduced a custom asynchronous keyword that accepts a closure and runs it in a separate thread. When applying this idea in real projects, thread‑pool configuration becomes critical, especially when the core pool size is greater than one or when the work queue size is mis‑configured.

Idea

A daemon thread is used to monitor the main thread and automatically shut down the asynchronous thread pool after the main method finishes. The pool is a fixed‑size executor with 16 threads, and the work queue is set to a large capacity (e.g., 100 000 or 1 000 000) to avoid blocking.

Step‑by‑step implementation

Create the thread pool

private static volatile ExecutorService funPool;

static ExecutorService getFunPool() {
    if (funPool == null) {
        synchronized (ThreadPoolUtil.class) {
            if (funPool == null) {
                funPool = createFixedPool(Constant.POOL_SIZE);
                daemon();
            }
        }
    }
    return funPool;
}

Create the daemon thread

static boolean daemon() {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (checkMain()) {
                SourceCode.sleep(1.0);
            }
            ThreadPoolUtil.shutFun();
        }
    });
    thread.setDaemon(true);
    thread.setName("FT-D");
    thread.start();
    logger.info("Daemon thread:{} started!", thread.getName());
    return true;
}

Check whether the main thread is alive

static boolean checkMain() {
    int count = Thread.activeCount();
    ThreadGroup group = Thread.currentThread().getThreadGroup();
    Thread[] threads = new Thread[count];
    group.enumerate(threads);
    for (int i = 0; i < count; i++) {
        if (threads[i].getName().equals("main")) {
            return true;
        }
    }
    return false;
}

Testing the asynchronous pool

Groovy test script (using the "times" sugar)

public static void main(String[] args) {
    20.times {
        def a = it as String
        fun {
            sleep(1.0)
            output(StringUtil.right("index:" + a, 10) + Time.getNow())
        }
    }
}

Equivalent Java version

public static void main(String[] args) {
    for (int i = 0; i < 20; i++) {
        Integer a = i;
        fun(() -> {
            sleep(1.0);
            output(StringUtil.right("index:" + a, 10) + Time.getNow());
            return null;
        });
    }
}

The console output shows each asynchronous task printing its index and timestamp, followed by a final warning line indicating that the daemon thread has closed the pool, which matches the expected behavior.

Thread synchronization with Phaser

To coordinate the completion of all asynchronous tasks, the article uses java.util.concurrent.Phaser. The Groovy and Java examples below demonstrate passing the phaser to the fun method.

Groovy version

public static void main(String[] args) {
    def phaser = new Phaser(1)
    20.times {
        def a = it as String;
        fun {
            sleep(1.0);
            output(StringUtil.right("index:" + a, 10) + Time.getNow());
        }, phaser;
    }
    phaser.arriveAndAwaitAdvance();
}

Java version

public static void main(String[] args) {
    Phaser phaser = new Phaser(1);
    for (int i = 0; i < 20; i++) {
        Integer a = i;
        fun(() -> {
            sleep(1.0);
            output(StringUtil.right("index:" + a, 10) + Time.getNow());
            return null;
        }, phaser);
    }
    phaser.arriveAndAwaitAdvance();
}

The final console logs again end with the daemon thread’s warning, confirming that the pool is closed only after all tasks have synchronized on the phaser.

Conclusion

Using a daemon thread to monitor the main thread provides a clean way to release resources for a custom asynchronous framework. Setting a sufficiently large work queue prevents task rejection, while Phaser offers a simple barrier for multi‑thread synchronization. The provided code snippets can be directly integrated into Java or Groovy projects that require high‑throughput batch execution.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencyThreadPoolAsynchronousGroovyPhaserDaemonThread
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

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.