Core Principles of Thread Pool
- Create a pool, the pool is initially empty
- When submitting tasks, the pool creates new thread objects. After task completion, threads are returned to the pool. When submitting tasks again, there’s no need to create new threads, existing threads can be reused directly
- However, if there are no idle threads in the pool when submitting tasks, and no new threads can be created, tasks will queue and wait
Creating Thread Pools
Thread pool creation mainly uses two methods from the Executors
utility class: newCachedThreadPool
and newFixedThreadPool
. The former creates a thread pool with no upper limit, while the latter creates a thread pool with an upper limit. Both return an ExecutorService
object:
ExecutorService e1 = Executors.newCachedThreadPool(); //Create an unlimited thread pool
ExecutorService e2 = Executors.newFixedThreadPool(3); //Create a thread pool that can contain at most 5 threads
Submitting Tasks to Thread Pool
Next, we’ll test whether the thread pool can really achieve the expected effect. Common methods for submitting tasks are submit
or execute
. Usually, execute(Runnable)
submits tasks for execution only without returning results, while submit(Runnable)
returns a Future
that can be used to get execution status or exception information.
We perform the following test:
public class Main {
public static void main(String[] args){
//Create threads
ExecutorService e1 = Executors.newCachedThreadPool(); //Create an unlimited thread pool
//ExecutorService e2 = Executors.newFixedThreadPool(5); //Create a thread pool that can contain at most 5 threads
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running.");
}
};
e1.submit(r);
}
}
As shown above, the thread is already executing. Notice that the program doesn’t end, so we need to destroy the thread pool when it’s no longer needed, although generally thread pools don’t need to be closed.
Let’s test with more threads:
public class Main {
public static void main(String[] args){
//Create threads
ExecutorService e1 = Executors.newCachedThreadPool(); //Create an unlimited thread pool
//ExecutorService e2 = Executors.newFixedThreadPool(5); //Create a thread pool that can contain at most 5 threads
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running.");
}
};
e1.submit(r);
e1.submit(r);
e1.submit(r);
e1.submit(r);
e1.submit(r);
}
}
As shown, we added 5 tasks. For further testing, we’ll add a short sleep before adding tasks to verify whether thread objects that have finished execution in the thread pool can be reused:
package top.mygld.demo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) throws InterruptedException {
//Create threads
ExecutorService e1 = Executors.newCachedThreadPool(); //Create an unlimited thread pool
//ExecutorService e2 = Executors.newFixedThreadPool(5); //Create a thread pool that can contain at most 5 threads
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running.");
}
};
e1.submit(r);
Thread.sleep(500);
e1.submit(r);
Thread.sleep(500);
e1.submit(r);
Thread.sleep(500);
e1.submit(r);
Thread.sleep(500);
e1.submit(r);
}
}
As shown above, thread 1 is reused multiple times after completion.
Now let’s test newFixedThreadPool
:
package top.mygld.demo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) throws InterruptedException {
//Create threads
//ExecutorService e1 = Executors.newCachedThreadPool(); //Create an unlimited thread pool
ExecutorService e2 = Executors.newFixedThreadPool(3); //Create a thread pool that can contain at most 5 threads
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running.");
}
};
e2.submit(r);
e2.submit(r);
e2.submit(r);
e2.submit(r);
e2.submit(r);
}
}
Perfect! As shown above, at most 3 threads can be used.
Thread Pool Destruction
For unused thread pools, simply use the shutdown
method to destroy them. No demonstration here.
Custom Thread Pool
Besides using factory methods provided by Executors
, we can also create a more flexible and controllable thread pool through ThreadPoolExecutor
. Its constructor is as follows:
public ThreadPoolExecutor(
int corePoolSize, // Core thread count
int maximumPoolSize, // Maximum thread count
long keepAliveTime, // Maximum survival time for non-core threads
TimeUnit unit, // Time unit for survival time
BlockingQueue<Runnable> workQueue, // Task queue
ThreadFactory threadFactory, // Thread factory for creating new threads
RejectedExecutionHandler handler // Rejection policy
)
Parameter Meanings
- corePoolSize Core thread count, the number of threads the thread pool will always maintain, even if these threads are idle.
- maximumPoolSize Maximum thread count, the maximum number of threads the thread pool can expand to when there are too many tasks.
- keepAliveTime & unit
Survival time for non-core threads (beyond
corePoolSize
) when idle, they will be recycled after timeout. - workQueue
Blocking queue for storing tasks waiting to be executed. Common choices:
ArrayBlockingQueue
(bounded queue, array implementation)LinkedBlockingQueue
(unbounded queue, linked list implementation)SynchronousQueue
(direct task submission, no storage)
- threadFactory Defines how threads are created, such as setting thread names, whether they are daemon threads, etc. Default factory is sufficient.
- handler
Rejection policy when both thread pool and queue are full:
AbortPolicy
(default, throws exception)CallerRunsPolicy
(caller thread executes the task)DiscardPolicy
(directly discard task)DiscardOldestPolicy
(discard oldest task in queue)
Example: Custom Thread Pool
package top.mygld.demo;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// Custom thread pool
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, // Core thread count
5, // Maximum thread count
10, TimeUnit.SECONDS, // Non-core thread idle survival time
new ArrayBlockingQueue<>(3), // Bounded task queue, stores at most 3 tasks
Executors.defaultThreadFactory(), // Default thread factory
new ThreadPoolExecutor.AbortPolicy() // Rejection policy: throw exception directly
);
Runnable task = () -> {
System.out.println(Thread.currentThread().getName() + " is executing task");
try {
Thread.sleep(2000); // Simulate time-consuming task
} catch (InterruptedException e) {
e.printStackTrace();
}
};
// Submit 10 tasks, observe how thread pool schedules them
for (int i = 1; i <= 10; i++) {
try {
pool.execute(task);
} catch (RejectedExecutionException e) {
System.out.println("Task " + i + " was rejected!");
}
}
pool.shutdown(); // Close thread pool
}
}
Execution Flow Explanation
- The first two tasks will directly start core threads for processing.
- The next three tasks will be placed in the task queue.
- When the queue is full, the thread pool will continue creating new threads (up to
maximumPoolSize=5
). - If both thread pool and queue are full, the 9th and 10th tasks will trigger the rejection policy, throwing exceptions directly and indicating rejection.
Maximum Parallelism
Meaning of Maximum Parallelism
- CPU Level: For compute-intensive tasks, maximum parallelism is usually limited by the number of CPU cores (including hyperthreading). For example, if your CPU has 8 cores (16 threads), theoretically executing 16 computing tasks simultaneously is optimal.
- Thread Pool Level:
For Java’s
ExecutorService
orThreadPoolExecutor
, maximum parallelism is the value of the thread pool’smaximumPoolSize
. The thread pool controls the number of simultaneously active threads based on this value. - Operating System Limitations: JVM thread count is also limited by the operating system. Usually, too many threads can cause memory overflow or excessive scheduling overhead.
Maximum parallelism can be obtained through the following code:
public class Main {
public static void main(String[] args) {
int i = Runtime.getRuntime().availableProcessors();
System.out.println(i);
}
}
Appropriate Thread Pool Size
CPU-Intensive Computing
Maximum parallelism + 1
I/O-Intensive Computing
Interview Key Points
- volatile
- JMM
- Pessimistic locks, optimistic locks, CAS
- Atomicity
- Concurrency utilities
These topics are frequently asked in interviews and will be organized later.