Fundamentals 9 min read

Master Java Multithreading and Custom Thread Pools in One Guide

This article explains why creating threads repeatedly wastes resources, introduces Java's ExecutorService thread pool, details its configuration parameters, demonstrates how to create and use fixed thread pools with Runnable and Callable tasks, shows proper shutdown, and highlights common pitfalls with Future.get.

The Dominant Programmer
The Dominant Programmer
The Dominant Programmer
Master Java Multithreading and Custom Thread Pools in One Guide

Creating and destroying threads consumes significant memory and CPU; repeatedly calling new Thread() can waste resources, cause excessive contention, and even crash the system. To avoid these problems, Java provides ExecutorService, a thread‑pool abstraction that reuses threads and controls concurrency. ExecutorService limits the maximum number of concurrent threads, improves resource utilization, prevents thread‑level contention, and offers scheduling features such as timed and periodic execution, eliminating the need for TimerTask.

One way to create a pool is to instantiate ThreadPoolExecutor directly:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

The parameters are:

corePoolSize : number of core threads that stay alive even when idle.

maximumPoolSize : upper limit of total threads; non‑core threads are terminated after keepAliveTime.

keepAliveTime : idle time after which non‑core threads are reclaimed.

unit : time unit for keepAliveTime (e.g., TimeUnit.SECONDS).

workQueue : queue that holds tasks waiting for a thread.

threadFactory : factory that creates new threads.

handler : policy invoked when the pool and queue are saturated.

A commonly used shortcut is the fixed‑size pool provided by Executors.newFixedThreadPool:

ExecutorService executorService = Executors.newFixedThreadPool(5);

The pool size (here 5) should be chosen based on the application's workload.

To use the pool, define a Runnable implementation, submit instances, and let the pool manage execution:

class PrintTask implements Runnable {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println("线程名:" + threadName + " 开始时间:" + DateUtils.getTime());
        System.out.println("系统需要业务操作3秒");
        try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("线程名:" + threadName + " 结束时间:" + DateUtils.getTime());
    }
}

ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
    executorService.submit(new PrintTask());
}

The GIF below shows that with 10 tasks and 5 threads the work finishes in two 3‑second batches; reducing the loop to 5 tasks completes in a single batch.

Thread pool execution demo
Thread pool execution demo

When the work is done, the pool can be shut down with shutdown(). In many real‑world scenarios the pool remains alive for the application's lifetime; calling shutdown() allows the main thread to exit after all tasks finish. executorService.shutdown(); For tasks that need to return a result, implement Callable<String> and override call():

class CustomTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        String threadName = Thread.currentThread().getName();
        System.out.println("线程名:" + threadName + " 开始时间:" + DateUtils.getTime());
        System.out.println("系统需要业务操作1秒");
        try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
        return "threadName = " + threadName + " 结束时间:" + DateUtils.getTime();
    }
}

ExecutorService executorService = Executors.newFixedThreadPool(5);
ArrayList<Future<String>> resultList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
    Future<String> submit = executorService.submit(new CustomTask());
    resultList.add(submit);
}
resultList.forEach(result -> {
    try { System.out.println(result.get()); }
    catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }
});

The Future.get() call blocks, so it should be invoked after all submissions; calling it inside the loop would serialize execution and defeat concurrency, as illustrated by the erroneous example and its GIF.

Blocking get example
Blocking get example
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.

JavaConcurrencyThreadPoolCallableExecutorServiceFuture
The Dominant Programmer
Written by

The Dominant Programmer

Resources and tutorials for programmers' advanced learning journey. Advanced tracks in Java, Python, and C#. Blog: https://blog.csdn.net/badao_liumang_qizhi

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.