# Comparison

## **About**

Java provides multiple ways to create and manage threads. Choosing the right method depends on our use case, code design, and requirements for maintainability and scalability.

This page compares different approaches to thread creation in Java, including extending the `Thread` class, implementing the `Runnable` interface etc.

Each approach has its strengths and trade-offs. Understanding these differences helps in writing cleaner, more efficient, and maintainable concurrent code.

## Extending `Thread` vs Implementing `Runnable`

<table data-header-hidden data-full-width="true"><thead><tr><th width="152.5616455078125"></th><th></th><th></th></tr></thead><tbody><tr><td>Criteria</td><td><strong>Extending <code>Thread</code></strong></td><td><strong>Implementing <code>Runnable</code></strong></td></tr><tr><td><strong>Design Flexibility</strong></td><td>Tightly couples our task logic with thread control, which violates separation of concerns.</td><td>Clean separation of task (logic) and thread management. We pass the task to a thread for execution.</td></tr><tr><td><strong>Inheritance Constraint</strong></td><td>Cannot extend any other class because Java supports only single inheritance.</td><td>Allows our class to extend another class if needed. Flexible for rich domain models.</td></tr><tr><td><strong>Code Reuse</strong></td><td>Difficult to reuse the thread class if we want the same task in multiple places.</td><td>Highly reusable — a single <code>Runnable</code> instance can be passed to multiple threads.</td></tr><tr><td><strong>Testability</strong></td><td>Harder to unit test because logic is embedded in thread management.</td><td>Logic is isolated in a simple functional unit (<code>run()</code>), making it easier to test.</td></tr><tr><td><strong>Readability</strong></td><td>Slightly cluttered because task and thread responsibilities are mixed.</td><td>Cleaner and more modular — responsibilities are clearer.</td></tr><tr><td><strong>Common Usage</strong></td><td>Mostly seen in basic tutorials or quick demos. Rarely used in real-world production systems.</td><td>Preferred in real applications, frameworks, and library-level abstractions.</td></tr></tbody></table>

## Manual Thread vs Executors

<table data-header-hidden data-full-width="true"><thead><tr><th width="172.73089599609375"></th><th></th><th></th></tr></thead><tbody><tr><td>Criteria</td><td><strong>Manual Thread Creation</strong></td><td><strong>Executors Framework (<code>ExecutorService</code>)</strong></td></tr><tr><td><strong>Thread Lifecycle</strong></td><td>We create and start each thread manually. We are responsible for managing their lifecycle explicitly.</td><td>Thread creation, reuse, and termination are managed internally by the thread pool.</td></tr><tr><td><strong>Scalability</strong></td><td>Poor for high-concurrency environments. Too many threads can exhaust system resources quickly.</td><td>Scales much better. Reuses a limited number of threads to handle a large number of tasks.</td></tr><tr><td><strong>Resource Utilization</strong></td><td>Inefficient. Each new thread consumes system memory and CPU separately.</td><td>Optimized. Threads are reused and scheduled intelligently to balance load.</td></tr><tr><td><strong>Code Complexity</strong></td><td>Simple for a small number of threads, but becomes unmanageable with more logic.</td><td>Slightly more verbose to set up initially, but far more maintainable as complexity grows.</td></tr><tr><td><strong>Error Handling</strong></td><td>Error-prone. We must handle exceptions, shutdown, and thread leaks manually.</td><td>Provides structured shutdown (<code>shutdown()</code>, <code>awaitTermination()</code>) and exception handling via <code>Future</code>.</td></tr><tr><td><strong>Real-world Use</strong></td><td>Not used in serious applications due to poor control and reliability.</td><td>Widely used in enterprise systems, Spring applications, and large-scale backends.</td></tr><tr><td><strong>Custom Thread Management</strong></td><td>Hard to integrate thread priorities, naming, etc., manually.</td><td>Supports <code>ThreadFactory</code> to customize threads easily.</td></tr><tr><td><strong>Example</strong></td><td><code>new Thread(() -> doTask()).start();</code></td><td><code>ExecutorService executor = Executors.newFixedThreadPool(10); executor.submit(() -> doTask());</code></td></tr></tbody></table>

## ExecutorService vs CompletableFuture

<table data-header-hidden data-full-width="true"><thead><tr><th width="175.5390625"></th><th></th><th></th></tr></thead><tbody><tr><td>Criteria</td><td><strong>ExecutorService</strong></td><td><strong>CompletableFuture (Java 8+)</strong></td></tr><tr><td><strong>Programming Style</strong></td><td>Imperative. We define task, submit it, then block to get the result via <code>Future.get()</code>.</td><td>Declarative and functional. We chain multiple operations and let the system handle scheduling.</td></tr><tr><td><strong>Result Handling</strong></td><td>Requires blocking to get result (<code>future.get()</code>), which defeats concurrency if used improperly.</td><td>Non-blocking. Results can be handled via <code>thenApply()</code>, <code>thenAccept()</code>, or <code>thenCompose()</code> without blocking.</td></tr><tr><td><strong>Error Handling</strong></td><td>We catch exceptions from <code>Future.get()</code> or within the task.</td><td>Supports chaining-based error handling via <code>exceptionally()</code> or <code>handle()</code>.</td></tr><tr><td><strong>Chaining</strong></td><td>No chaining support. Each task is isolated.</td><td>Built for pipelines — we can compose multiple async tasks fluently.</td></tr><tr><td><strong>Thread Management</strong></td><td>Requires an explicit thread pool. We must submit tasks and manage shutdown.</td><td>Uses ForkJoinPool by default. We can also provide a custom executor.</td></tr><tr><td><strong>Readability &#x26; Structure</strong></td><td>Procedural and somewhat rigid, especially for complex workflows.</td><td>Highly readable and expressive for dependent tasks and async sequences.</td></tr><tr><td><strong>Parallel Composition</strong></td><td>We have to manage multiple <code>Future</code>s manually using lists or loops.</td><td>Provides <code>allOf()</code> and <code>anyOf()</code> for parallel composition of multiple tasks.</td></tr><tr><td><strong>Best Use Case</strong></td><td>Classic multi-threading: batch processing, independent tasks, producer-consumer patterns.</td><td>Async workflows, dependent async tasks, non-blocking services, API orchestration.</td></tr><tr><td><strong>Example</strong></td><td><code>Future&#x3C;String> f = executor.submit(callable); String result = f.get();</code></td><td><code>CompletableFuture.supplyAsync(() -> compute()).thenApply(res -> transform(res)).thenAccept(System.out::println);</code></td></tr></tbody></table>
