Java
concurrency

Why You Should Use ExecutorService Instead of Creating Threads Manually

Sarvar Musazadeviews

Why You Should Use ExecutorService Instead of Creating Threads Manually

Creating threads is easy. Managing them efficiently is not.

When developers first learn Java concurrency, they often write asynchronous code like this:

new Thread(() -> {
sendEmail();
}).start();

It works.

The email is sent on another thread, and the main thread continues executing.

So why do experienced Java developers almost never create threads manually?

The answer is ExecutorService.

In this article, we'll explore what ExecutorService is, why it's the preferred approach in modern Java applications, and how it solves problems that manual thread creation introduces.

The Problem with Manual Threads

Suppose you're building a Spring Boot application.

Every time a user registers, you want to send a welcome email.

A beginner might write:

public void register(User user) {
saveUser(user);

new Thread(() -> emailService.sendWelcomeEmail(user)).start();
}

It looks perfectly fine.

Now imagine your application receives 10,000 registration requests.

That code creates 10,000 operating system threads.

Request 1 -> Thread 1
Request 2 -> Thread 2
Request 3 -> Thread 3
...
Request 10000 -> Thread 10000

This creates several problems.

Threads are not free.

Each thread requires memory for its stack and must be scheduled by the operating system.

Creating thousands of them can quickly exhaust system resources.

What is ExecutorService?

ExecutorService is a Java framework that manages a pool of reusable worker threads.

Instead of creating a new thread for every task, you submit tasks to an executor.

ExecutorService executor =
Executors.newFixedThreadPool(5);

executor.submit(() -> sendEmail());

Notice something important.

You never create a thread yourself.

Instead, you describe the work, and the executor decides which thread should execute it.

This separation between task and thread is one of the biggest advantages of the Executor framework.

How ExecutorService Works

Imagine you configure a thread pool with five workers.

ExecutorService executor =
Executors.newFixedThreadPool(5);

Now your application receives 100 email requests.

100 Email Tasks


+----------------+
| Task Queue |
+----------------+


Worker 1
Worker 2
Worker 3
Worker 4
Worker 5

Only five threads are created.

Every new task waits in the queue until one worker becomes available.

When a worker finishes a task, it immediately starts processing the next one.

No additional threads need to be created.

Thread Reuse

Without ExecutorService, every task creates a new thread.

Create Thread

Execute Task

Destroy Thread

Repeat that thousands of times.

With an executor:

Create Worker Threads Once

Execute Task

Execute Task

Execute Task

Execute Task

The same threads are reused for many tasks.

This significantly reduces overhead.

Better Resource Management

One of the biggest benefits of ExecutorService is controlling concurrency.

Imagine your email provider only allows five simultaneous connections.

With manual threads:

500 Threads


Email Server

The email server may become overloaded.

With a thread pool:

ExecutorService executor =
Executors.newFixedThreadPool(5);

Only five tasks execute simultaneously.

The remaining tasks simply wait in the queue.

This protects both your application and external systems.

Tasks vs Threads

This is a concept every Java developer should understand.

A Thread is a worker.

A Runnable or Callable is a task.

With manual threads:

new Thread(task).start();

You create both the worker and the task together.

With ExecutorService:

executor.submit(task);

You only provide the task.

The executor already owns the workers.

This makes your code cleaner and more maintainable.

Returning Results

Another limitation of manually created threads is that they don't naturally return values.

With ExecutorService, you can submit a Callable.

Future<Integer> future =
executor.submit(() -> calculateTotal());

Integer total = future.get();

Future allows you to:

This is much more powerful than using a plain Thread.

Graceful Shutdown

Threads should not run forever.

ExecutorService provides lifecycle management.

executor.shutdown();

executor.awaitTermination(
30,
TimeUnit.SECONDS
);

This allows the application to stop gracefully after completing pending work.

Managing dozens or hundreds of manually created threads is much more difficult.

Exception Handling

Suppose an exception occurs.

executor.submit(() -> {
throw new RuntimeException("Email failed");
});

The exception is stored inside the returned Future.

try {
future.get();
} catch (ExecutionException e) {
System.out.println(e.getCause());
}

With manually created threads, exceptions occur on another thread and are much harder to manage.

A Real Spring Boot Example

Imagine an application that sends notifications.

Instead of:

for (User user : users) {
new Thread(() ->
notificationService.send(user))
.start();
}

Use:

ExecutorService executor =
Executors.newFixedThreadPool(10);

for (User user : users) {
executor.submit(() ->
notificationService.send(user));
}

The executor processes notifications efficiently while limiting concurrency.

Should You Use Executors Utility Methods?

Many examples use:

Executors.newFixedThreadPool(10)

This is great for learning.

However, in production, many teams prefer creating a ThreadPoolExecutor directly because it provides better control over queue size, rejection policies, and thread limits.

For example:

ExecutorService executor =
new ThreadPoolExecutor(
5,
10,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);

This configuration prevents unlimited task accumulation and provides backpressure when the system becomes overloaded.

When Should You Still Create Threads Manually?

There are a few situations where manually creating a thread is acceptable.

For almost every production application, however, ExecutorService is the better choice.

Key Takeaways

✅ Reuses threads instead of constantly creating new ones.

✅ Limits the number of concurrent tasks.

✅ Reduces memory usage and context switching.

✅ Works naturally with Future and Callable.

✅ Provides lifecycle management.

✅ Makes applications more scalable.

✅ Separates tasks from threads.

Final Thoughts

Thread represents how work is executed.

ExecutorService represents how work should be managed.

Modern Java applications rarely create threads directly because thread management is a complex problem that has already been solved by the Executor framework.

The next time you find yourself writing:

new Thread(...).start();

Ask yourself:

Should I really create another thread, or should I submit this task to an ExecutorService instead?

Most of the time, the answer is ExecutorService.

If you found this article helpful, consider following me for more deep dives into Java, Spring Boot, system design, and backend architecture.

Comments (0)

0/1000

Loading comments...