Modern Java

Executors, Callable, Future, and CompletableFuture

ReadingPreview

You are viewing a free preview lesson.

Lesson Overview

আগের lesson-এ আমরা Thread, shared state, race condition, synchronized, volatile, এবং thread safety-এর foundation শিখেছি।

Raw thread create করা সম্ভব:

Thread worker =
        new Thread(
                () ->
                        doWork()
        );

worker.start();

কিন্তু real applications-এ প্রতিটি task-এর জন্য manually:

Thread বানানো
start করা
lifecycle manage করা
error handle করা
result collect করা

quickly difficult হয়ে যায়।

Java তাই task execution-এর জন্য higher-level abstractions দেয়:

Executor
ExecutorService
Runnable
Callable<T>
Future<T>
CompletableFuture<T>

এই lesson-এ আমরা শিখব:

  • Task এবং Thread-এর difference
  • কেন raw Thread সবসময় best abstraction নয়
  • ExecutorService
  • execute()
  • submit()
  • Runnable
  • Callable<T>
  • Future<T>
  • get()
  • isDone()
  • cancel()
  • Executor shutdown
  • CompletableFuture
  • runAsync()
  • supplyAsync()
  • thenApply()
  • thenAccept()
  • thenRun()
  • thenCompose()
  • thenCombine()
  • exceptionally()
  • handle()
  • Blocking বনাম asynchronous composition
  • Custom Executor
  • Common concurrency mistakes

Task vs Thread

এটি প্রথম important distinction।

A:

Task

মানে:

কী কাজ করতে হবে

A:

Thread

মানে:

কোন execution resource সেই কাজ চালাবে

Example task:

() ->
        generateReport()

এই behavior বলে:

কী করতে হবে

কিন্তু এটি নিজে বলে না:

কোন Thread চালাবে
কখন চালাবে

Why Separate Task from Thread?

Raw Thread:

new Thread(
        task
).start();

এখানে task এবং execution mechanism tightly coupled।

Executor-based model:

Task
↓
Executor
↓
Execution resource

Now application code বলে:

এই task execute করো

Executor decide করে execution কীভাবে manage হবে।


Why Not Create Unlimited Threads?

Suppose server receives:

10,000 tasks

এবং আমরা blindly:

new Thread(...)

10,000 times করি।

Every platform thread has runtime/OS resource cost।

Too many threads can cause:

high memory use
context switching
resource exhaustion
poor performance
unstable application behavior

তাই traditionally thread pools use করা হয়।

Later lesson-এ আমরা Virtual Threads দেখব, যেগুলোর resource model আলাদা।


Runnable

Runnable একটি functional interface।

Its method:

void run();

Mental model:

() → void

Example:

Runnable task =
        () ->
                System.out.println(
                        "Processing course"
                );

Runnable Has No Result

A Runnable:

input নেয় না
result return করে না

It represents:

do this work

Example:

Runnable cleanupTask =
        () ->
                cleanupTemporaryFiles();

ExecutorService

ExecutorService tasks execute এবং lifecycle manage করার higher-level API।

Import:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

Example:

ExecutorService executor =
        Executors.newFixedThreadPool(
                4
        );

এখানে একটি fixed-size thread pool তৈরি হলো।


Fixed Thread Pool Mental Model

Task Queue
↓
Worker Thread 1
Worker Thread 2
Worker Thread 3
Worker Thread 4

যদি 10টি task আসে:

4 workers available tasks execute করবে
remaining tasks wait করতে পারে

Workers free হলে next tasks execute করবে।


Execute a Runnable

executor.execute(
        () ->
                System.out.println(
                        "Running task"
                )
);

execute() accepts:

Runnable

Important: Executor Must Be Shut Down

এই code:

ExecutorService executor =
        Executors.newFixedThreadPool(
                4
        );

resources create করে।

কাজ শেষ হলে lifecycle properly close করতে হবে।

Basic pattern:

ExecutorService executor =
        Executors.newFixedThreadPool(
                4
        );

try {
    executor.execute(
            () ->
                    doWork()
    );
} finally {
    executor.shutdown();
}

What Does shutdown() Mean?

executor.shutdown();

means conceptually:

নতুন task accept করা বন্ধ করো

already submitted tasks
finish করতে দাও

এটি immediately সব tasks kill করে না।


shutdownNow()

There is also:

executor.shutdownNow();

এটি running tasks interrupt করার চেষ্টা করে এবং queued tasks-এর handling আলাদা হতে পারে।

এটি guaranteed forced termination mechanism নয়।

Interrupt cooperation still matters।

Foundation rule:

Use shutdown intentionally,
and design task interruption behavior deliberately.

execute() vs submit()

execute():

executor.execute(
        runnable
);

simple fire-and-execute style।

submit():

Future<?> future =
        executor.submit(
                runnable
        );

returns:

Future

যার মাধ্যমে task completion/cancellation inspect করা যায়।


Callable<T>

Sometimes task needs to return a result।

Runnable cannot return a value।

Use:

Callable<T>

Import:

import java.util.concurrent.Callable;

Its method:

T call()
        throws Exception;

Mental model:

() → T

Callable Example

Callable<Integer> task =
        () -> 10 + 20;

This task returns:

Integer

Runnable vs Callable

Runnable
→ () → void

Callable<T>
→ () → T

Another difference:

Callable.call()

can declare checked exceptions।

This makes it useful for tasks that produce values and may fail।


Submit a Callable

Future<Integer> future =
        executor.submit(
                () -> 10 + 20
        );

submit() immediately gives:

Future<Integer>

Not necessarily actual result yet।


What Is Future<T>?

Future<T> represents:

a result that may become available later

Mental model:

Task submitted
↓
work may still be running
↓
Future represents eventual outcome

Getting the Result

Integer result =
        future.get();

If task not finished yet:

current thread waits

until result becomes available or task fails/interruption occurs।


Future.get() Is Blocking

This is critical।

future.get();

does not mean:

non-blocking async magic

If result is not ready, calling thread blocks।

So:

task is asynchronous

and:

caller waits synchronously with get()

can both be true।


Example

Future<String> future =
        executor.submit(
                () -> {
                    Thread.sleep(
                            1_000
                    );

                    return "Done";
                }
        );

System.out.println(
        "Task submitted"
);

String result =
        future.get();

System.out.println(
        result
);

Conceptually:

submit task
↓
caller continues
↓
get()
↓
wait if necessary
↓
result

Future Exception Handling

get() may throw checked exceptions such as:

InterruptedException
ExecutionException

ExecutionException wraps failure that occurred inside the task।


Example

Future<Integer> future =
        executor.submit(
                () -> {
                    throw new IllegalStateException(
                            "Calculation failed."
                    );
                }
        );

Then:

future.get();

will not directly return result।

The task failure is surfaced through ExecutionException


Inspect the Cause

try {
    future.get();
} catch (
        ExecutionException exception
) {
    Throwable cause =
            exception.getCause();

    System.out.println(
            cause.getMessage()
    );
}

Handle Interruption Properly

Example:

try {
    return future.get();
} catch (
        InterruptedException exception
) {
    Thread.currentThread()
            .interrupt();

    throw new IllegalStateException(
            "Interrupted while waiting.",
            exception
    );
}

If your code cannot fully handle cancellation, restoring interrupt status is an important pattern।


isDone()

Check whether task finished:

if (
        future.isDone()
) {
    ...
}

But avoid polling in tight loops like:

while (
        !future.isDone()
) {
}

This can waste CPU।

Use appropriate coordination/composition APIs।


cancel()

A Future may support cancellation request:

future.cancel(
        true
);

The boolean indicates whether interruption may be attempted if task is running।

Cancellation is cooperative।

Task code may need to respond to interruption।


Cancellation Is Not Forced Destruction

Java generally does not safely "kill" arbitrary thread execution at any instruction।

A task should cooperate by:

responding to interrupt
using interruptible blocking APIs
checking interruption when appropriate
cleaning up resources

Complete Executor + Callable Example

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {

    public static void main(
            String[] args
    ) {
        ExecutorService executor =
                Executors.newFixedThreadPool(
                        2
                );

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

            System.out.println(
                    "Calculation submitted"
            );

            int total =
                    waitFor(
                            future
                    );

            System.out.println(
                    "Total: "
                    + total
            );
        } finally {
            executor.shutdown();
        }
    }

    static int calculateTotal() {
        return 10
                + 20
                + 30;
    }

    static int waitFor(
            Future<Integer> future
    ) {
        try {
            return future.get();
        } catch (
                InterruptedException exception
        ) {
            Thread.currentThread()
                    .interrupt();

            throw new IllegalStateException(
                    "Interrupted while waiting.",
                    exception
            );
        } catch (
                ExecutionException exception
        ) {
            throw new IllegalStateException(
                    "Task failed.",
                    exception.getCause()
            );
        }
    }
}

The Limitation of Future

Future<T> useful হলেও composition awkward।

Suppose:

Task A
↓ result
Task B uses A
↓ result
Task C uses B

Using only:

Future.get()

we often end up manually blocking:

A.get()
B.get()
C.get()

This makes asynchronous workflows harder to compose।


Enter CompletableFuture<T>

CompletableFuture<T> represents an asynchronous computation that can be:

completed
transformed
combined
chained
recovered

Import:

import java.util.concurrent.CompletableFuture;

It is both a Future-like result and a composition API।


runAsync()

For a task with no return value:

CompletableFuture<Void> future =
        CompletableFuture.runAsync(
                () ->
                        doWork()
        );

Mental model:

async Runnable

supplyAsync()

For task with result:

CompletableFuture<String> future =
        CompletableFuture.supplyAsync(
                () ->
                        loadCourseTitle()
        );

Mental model:

async Supplier<T>

runAsync() vs supplyAsync()

runAsync()
→ no result
→ CompletableFuture<Void>

supplyAsync()
→ returns value
→ CompletableFuture<T>

Getting CompletableFuture Result

You can still block:

String result =
        future.join();

or:

future.get();

But composition APIs often let us avoid blocking between stages।


join() vs get()

Both can wait for completion।

get() uses checked exception handling from Future

join() surfaces completion failure through unchecked completion-related exception semantics।

Use deliberately।

The bigger lesson:

Don't immediately call join/get
after every async stage.
Compose stages when possible.

Bad Async Pattern

String title =
        CompletableFuture.supplyAsync(
                () ->
                        loadTitle()
        ).join();

This may still be useful in some boundaries।

But if done immediately every time:

start async
then immediately block

you may gain little from asynchronous composition।


thenApply()

Suppose async task returns:

Course

Then transform it to:

String title

Use:

CompletableFuture<String> titleFuture =
        courseFuture.thenApply(
                Course::title
        );

This is similar to:

Optional.map()
Stream.map()

Mental model:

T
→ R

Example

CompletableFuture<Integer> lengthFuture =
        CompletableFuture.supplyAsync(
                () ->
                        "Java Foundation"
        ).thenApply(
                String::length
        );

Flow:

String
↓
Integer

Chaining Transformations

CompletableFuture<String> future =
        CompletableFuture.supplyAsync(
                () ->
                        " java "
        )
                .thenApply(
                        String::strip
                )
                .thenApply(
                        String::toUpperCase
                );

Result eventually:

JAVA

thenAccept()

If previous result আছে, but next stage only consumes it:

future.thenAccept(
        System.out::println
);

Mental model:

T
→ void

This aligns with:

Consumer<T>

Example

CompletableFuture<Void> printed =
        CompletableFuture.supplyAsync(
                () ->
                        "Java"
        ).thenAccept(
                System.out::println
        );

thenRun()

If next stage does not need previous result:

future.thenRun(
        () ->
                System.out.println(
                        "Finished"
                )
);

Mental model:

previous completion
↓
run another Runnable

The previous result is ignored।


thenApply() vs thenAccept() vs thenRun()

thenApply()
→ use result and return another result

thenAccept()
→ use result, return no meaningful result

thenRun()
→ don't use previous result, just run next task

thenCompose()

This is one of the most important CompletableFuture methods।

Suppose:

CompletableFuture<Course> findCourse(
        String code
)

and:

CompletableFuture<List<Lesson>> loadLessons(
        Course course
)

If we use:

courseFuture.thenApply(
        this::loadLessons
)

result becomes:

CompletableFuture<CompletableFuture<List<Lesson>>>

Nested future।


Use thenCompose()

CompletableFuture<List<Lesson>> lessonsFuture =
        courseFuture.thenCompose(
                this::loadLessons
        );

It flattens:

Future of Future

into:

one CompletableFuture

Familiar Pattern

Recall:

Optional.map()
Optional.flatMap()

Stream.map()
Stream.flatMap()

Same conceptual family:

thenApply()
→ transform value

thenCompose()
→ transform into another CompletableFuture and flatten

Example

static CompletableFuture<Course> findCourse(
        String code
) {
    return CompletableFuture.supplyAsync(
            () ->
                    new Course(
                            code,
                            "Java Foundation"
                    )
    );
}

static CompletableFuture<String> loadDescription(
        Course course
) {
    return CompletableFuture.supplyAsync(
            () ->
                    "Description for "
                    + course.title()
    );
}

Chain:

CompletableFuture<String> descriptionFuture =
        findCourse(
                "JAVA"
        ).thenCompose(
                Main::loadDescription
        );

thenCombine()

Sometimes two independent async tasks can run separately, then combine results।

Example:

Load Course
Load Enrollment Count

Neither depends on the other।


Start Both

CompletableFuture<Course> courseFuture =
        CompletableFuture.supplyAsync(
                () ->
                        loadCourse()
        );

CompletableFuture<Integer> countFuture =
        CompletableFuture.supplyAsync(
                () ->
                        loadEnrollmentCount()
        );

Combine:

CompletableFuture<String> summaryFuture =
        courseFuture.thenCombine(
                countFuture,
                (
                        course,
                        count
                ) ->
                        course.title()
                        + " - "
                        + count
                        + " learners"
        );

Why thenCombine() Is Useful

Sequential dependency:

A
↓
B

use:

thenCompose()

Independent tasks:

A ─┐
   ├→ combine
B ─┘

use:

thenCombine()

Sequential vs Independent Work

Bad:

Course course =
        loadCourse();

int count =
        loadEnrollmentCount();

If both operations are independent and slow I/O-like tasks, async overlap may reduce total waiting time।

But:

async does not automatically mean faster

There is overhead and resource management to consider।


exceptionally()

If asynchronous stage fails, fallback result দিতে:

CompletableFuture<String> future =
        CompletableFuture.supplyAsync(
                () ->
                        loadTitle()
        ).exceptionally(
                exception ->
                        "Unknown"
        );

If successful:

actual value

If failed:

Unknown

Do Not Swallow Important Failures

This is dangerous:

.exceptionally(
        exception ->
                null
)

for every failure।

Now caller may not know:

not found?
network failure?
programming bug?
timeout?

Fallback only when domain semantics justify it।


handle()

handle() runs for:

success
or
failure

It receives:

result
exception

Example:

CompletableFuture<String> safe =
        future.handle(
                (
                        value,
                        exception
                ) -> {
                    if (
                            exception != null
                    ) {
                        return "Unavailable";
                    }

                    return value;
                }
        );

exceptionally() vs handle()

Simplified:

exceptionally()
→ mainly recover from failure

handle()
→ inspect both success and failure

Keep Exception Semantics Clear

Suppose:

Course not found

is normal absence।

That may be better represented as:

Optional<Course>

inside future:

CompletableFuture<Optional<Course>>

rather than throwing and recovering everything through exceptionally()

Error modeling and asynchronous execution are separate design decisions।


CompletableFuture with Custom Executor

By default, async methods without an explicit Executor use a runtime-provided execution facility।

For production control, you may provide your own Executor:

ExecutorService executor =
        Executors.newFixedThreadPool(
                4
        );

Then:

CompletableFuture<Course> future =
        CompletableFuture.supplyAsync(
                () ->
                        loadCourse(),
                executor
        );

Why Provide an Executor?

It gives control over:

resource ownership
concurrency limits
lifecycle
task isolation

This is especially important when tasks have known operational characteristics।


Executor Ownership Matters

If your code creates:

ExecutorService executor

your code generally needs a clear lifecycle responsibility for shutting it down।

Avoid creating new thread pools casually inside frequently called methods।

Bad:

void process() {
    ExecutorService executor =
            Executors.newFixedThreadPool(
                    10
            );

    ...
}

called thousands of times।

This can create unnecessary resource churn।


Better Ownership

Create executor at application/component lifecycle boundary।

Pass or inject it where needed।

Conceptually:

Application startup
→ create executor

Application runtime
→ submit tasks

Application shutdown
→ shutdown executor

Async Does Not Remove Thread-Safety Problems

Suppose:

CompletableFuture.runAsync(
        () ->
                counter++
);

Running asynchronously does not make:

counter++

safe।

Same shared-state rules still apply।


Async Can Increase Concurrency Exposure

Sequential code:

first();
second();
third();

may accidentally avoid races because only one execution flow modifies state।

After turning operations async:

runAsync(...)
runAsync(...)
runAsync(...)

shared mutable state may now be accessed concurrently।

So:

more async
→ more need for clear state ownership

Avoid Blocking Inside Every Stage

Consider:

CompletableFuture<Course> courseFuture =
        loadCourseAsync();

Course course =
        courseFuture.join();

CompletableFuture<List<Lesson>> lessonFuture =
        loadLessonsAsync(
                course
        );

List<Lesson> lessons =
        lessonFuture.join();

This serializes waiting manually।

Better:

CompletableFuture<List<Lesson>> lessonsFuture =
        loadCourseAsync()
                .thenCompose(
                        this::loadLessonsAsync
                );

Composition Keeps Dependency Explicit

loadCourseAsync()
        .thenCompose(
                this::loadLessonsAsync
        )
        .thenApply(
                lessons ->
                        lessons.size()
        );

Flow:

Course
↓
Lessons
↓
Count

without blocking between stages।


Blocking Is Sometimes Necessary at a Boundary

Eventually a synchronous caller may need the actual result।

At that boundary:

future.join();

may be appropriate।

The issue is not:

never block

The issue is:

don't block unnecessarily between stages
when asynchronous composition can express the flow.

CPU-Bound vs Waiting Tasks

Concurrency strategy should consider task nature।

CPU-bound work:

heavy computation
compression
parsing
image processing

cannot become infinitely faster by adding threads।

CPU capacity is limited।

Waiting/blocking work:

waiting on file
network
external service

can benefit from higher concurrency because threads/tasks may spend time waiting।

Later Virtual Threads lesson expands this distinction।


Do Not Assume More Threads = More Performance

For CPU-bound work:

1000 worker threads

on a small number of CPU cores can increase overhead rather than throughput।

Concurrency level must match workload।


CompletableFuture.allOf()

When multiple futures need completion:

CompletableFuture<Void> all =
        CompletableFuture.allOf(
                first,
                second,
                third
        );

This future completes when all supplied futures complete։


Important: allOf() Does Not Directly Return All Typed Values

allOf() result is:

CompletableFuture<Void>

You still retrieve each individual future's result if needed।

Example:

all.join();

String firstResult =
        first.join();

String secondResult =
        second.join();

After all completes, those joins should no longer need to wait for unfinished work, though failures still need handling।


CompletableFuture.anyOf()

There is also:

CompletableFuture.anyOf(
        first,
        second
);

It completes when one supplied future completes।

Its result typing is more generic:

CompletableFuture<Object>

Use it only when "first completion wins" matches the actual requirement।


Timeouts — Conceptual Note

Asynchronous tasks can hang or take too long।

Production systems often need timeout policies।

Modern CompletableFuture APIs include timeout-oriented capabilities, but the important design lesson is:

Every remote or potentially unbounded wait
should have an intentional timeout strategy.

Do not assume async means:

it will finish eventually.

Resource Lifetime Across Async Tasks

If asynchronous task uses:

file
stream
connection
transaction-like resource

ensure resource lifetime actually covers task execution।

Bad conceptual pattern:

open resource
schedule async task using it
close resource immediately

Then async task may access closed resource।

Ownership and lifetime remain important।


ThreadLocal Context Warning

Some application context may be attached to a particular thread।

If execution moves to another thread through:

CompletableFuture.supplyAsync(...)

do not assume thread-bound context automatically follows।

Examples can include:

request-local context
logging context
security context
transaction context

Framework behavior varies।

General lesson:

async thread switch is an execution-boundary.

Do not rely on hidden thread state unless your environment explicitly supports propagation।


Complete CompletableFuture Example

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {

    public static void main(
            String[] args
    ) {
        ExecutorService executor =
                Executors.newFixedThreadPool(
                        4
                );

        try {
            CompletableFuture<Course> courseFuture =
                    CompletableFuture.supplyAsync(
                            () ->
                                    loadCourse(
                                            "JAVA"
                                    ),
                            executor
                    );

            CompletableFuture<Integer> enrollmentFuture =
                    CompletableFuture.supplyAsync(
                            Main::loadEnrollmentCount,
                            executor
                    );

            CompletableFuture<String> summaryFuture =
                    courseFuture.thenCombine(
                            enrollmentFuture,
                            (
                                    course,
                                    enrollmentCount
                            ) ->
                                    course.title()
                                    + " has "
                                    + enrollmentCount
                                    + " learners."
                    );

            String summary =
                    summaryFuture.join();

            System.out.println(
                    summary
            );
        } finally {
            executor.shutdown();
        }
    }

    static Course loadCourse(
            String code
    ) {
        return new Course(
                code,
                "Java Foundation"
        );
    }

    static int loadEnrollmentCount() {
        return 120;
    }

    record Course(
            String code,
            String title
    ) {
    }
}

Complete Sequential Dependency Example

import java.util.List;
import java.util.concurrent.CompletableFuture;

public class Main {

    public static void main(
            String[] args
    ) {
        CompletableFuture<Integer> lessonCountFuture =
                findCourse(
                        "JAVA"
                )
                        .thenCompose(
                                Main::loadLessons
                        )
                        .thenApply(
                                List::size
                        );

        System.out.println(
                lessonCountFuture.join()
        );
    }

    static CompletableFuture<Course> findCourse(
            String code
    ) {
        return CompletableFuture.completedFuture(
                new Course(
                        code,
                        "Java Foundation"
                )
        );
    }

    static CompletableFuture<List<Lesson>> loadLessons(
            Course course
    ) {
        return CompletableFuture.completedFuture(
                List.of(
                        new Lesson(
                                "Variables"
                        ),
                        new Lesson(
                                "Methods"
                        ),
                        new Lesson(
                                "OOP"
                        )
                )
        );
    }

    record Course(
            String code,
            String title
    ) {
    }

    record Lesson(
            String title
    ) {
    }
}

Flow:

find Course
↓
load Lessons
↓
count Lessons

completedFuture()

Sometimes a value is already available but API expects:

CompletableFuture<T>

Use:

CompletableFuture.completedFuture(
        value
);

This creates an already-completed future।

Useful in:

tests
fallback branches
simple implementations
conditional async APIs

Do Not Fake Async Without Reason

Wrapping every synchronous method:

CompletableFuture.supplyAsync(
        () ->
                simpleCalculation()
)

does not automatically improve design।

You add:

scheduling
thread switching
exception wrapping
execution context complexity

Use asynchronous execution when there is a real concurrency/asynchrony benefit।


Common Mistake 1 — Creating Raw Threads for Every Task

Possible for simple examples।

But application-level task execution usually benefits from higher-level execution management।


Common Mistake 2 — Forgetting Executor Shutdown

A created executor has lifecycle and resources।

Always define ownership and shutdown strategy।


Common Mistake 3 — Calling get() Immediately

Future<T> future =
        executor.submit(
                task
        );

T value =
        future.get();

can make the flow effectively synchronous from caller perspective।

Maybe correct, but understand the consequence।


Common Mistake 4 — Busy Waiting

Avoid:

while (
        !future.isDone()
) {
}

This wastes CPU।

Use proper waiting/composition।


Common Mistake 5 — Swallowing Task Exceptions

Bad:

try {
    future.get();
} catch (
        Exception exception
) {
}

Now failures disappear।

Preserve meaningful failure information।


Common Mistake 6 — Swallowing Interruption

Bad:

catch (
        InterruptedException exception
) {
    // ignore
}

Handle interruption deliberately।


Common Mistake 7 — Confusing thenApply() and thenCompose()

If mapper returns:

R

use:

thenApply()

If mapper returns:

CompletableFuture<R>

use:

thenCompose()

Common Mistake 8 — Nested Futures

If you see:

CompletableFuture<CompletableFuture<T>>

check whether:

thenCompose()

is needed।


Common Mistake 9 — Async Shared Mutation

tasks.forEach(
        task ->
                CompletableFuture.runAsync(
                        () ->
                                sharedList.add(
                                        task
                                )
                )
);

A normal shared collection may not be safe।

Async execution does not remove thread-safety requirements।


Common Mistake 10 — Too Many Executors

Creating a new thread pool for each request/task can exhaust resources।

Executor lifecycle should usually exist at a larger component/application scope।


Common Mistake 11 — Assuming Async Means Parallel

An asynchronous stage may:

run later
run on another thread
wait for resources
share executor capacity

It does not guarantee physical parallel execution।


Common Mistake 12 — Assuming Async Means Faster

For small computations:

scheduling overhead

may make async slower।

For CPU-bound work, available cores still limit execution।


Common Mistake 13 — Blocking an Executor with Wrong Workload

A small pool filled with long blocking tasks can prevent other queued tasks from progressing quickly।

Executor configuration must reflect workload।

Virtual Threads later provide another model for high-concurrency blocking tasks।


Common Mistake 14 — Unbounded Async Fan-Out

Dangerous conceptually:

for (
        Item item
        : millionsOfItems
) {
    CompletableFuture.runAsync(
            () ->
                    process(
                            item
                    )
    );
}

Submitting unlimited work can create memory/resource pressure even if execution is asynchronous।

Concurrency still requires:

backpressure
limits
resource awareness

Common Mistake 15 — Recovering Every Failure with a Default

.exceptionally(
        exception ->
                "OK"
)

may turn real failures into fake success।

Fallback must reflect domain semantics।


Practice 1 — Runnable or Callable?

Task:

Print a message
No result needed

Answer

Runnable

Practice 2

Task:

Calculate total price
Return long

Answer

Callable<Long>

Practice 3 — Future

What does:

Future<Course>

represent?

Answer

A Course result that may complete later।


Practice 4 — Blocking

Does:

future.get()

block if result is not ready?

Answer

Yes।


Practice 5 — Executor Lifecycle

After submitting all intended tasks and finishing application usage, what should happen to an owned ExecutorService?

Answer

It should have an intentional shutdown strategy, commonly:

shutdown()

for orderly shutdown।


Practice 6 — runAsync() or supplyAsync()?

Task:

send a notification
no return result

Answer

runAsync()

Practice 7

Task:

load Course
returns Course

Answer

supplyAsync()

Practice 8 — thenApply()

Given:

CompletableFuture<Course>

Need:

CompletableFuture<String>

using Course::title.

Solution

courseFuture.thenApply(
        Course::title
);

Practice 9 — thenCompose()

Given:

CompletableFuture<Course>

and:

CompletableFuture<List<Lesson>> loadLessons(
        Course course
)

Which operation?

Answer

thenCompose()

Practice 10 — Why Not thenApply()?

Because:

thenApply(
        this::loadLessons
)

would produce:

CompletableFuture<
        CompletableFuture<List<Lesson>>
>

Practice 11 — Combine Independent Results

Have:

CompletableFuture<Course>

and:

CompletableFuture<Integer>

Need one summary after both complete।

Answer

thenCombine()

Practice 12 — Consume Result

Need to print result and produce no new value।

Answer

thenAccept(
        System.out::println
)

Practice 13 — Run After Completion

Need:

print "Done"

after a future completes, but previous result is irrelevant।

Answer

thenRun(
        () ->
                System.out.println(
                        "Done"
                )
);

Practice 14 — Error Recovery

Need a fallback value only if computation fails।

Answer

exceptionally(...)

can be appropriate।

But fallback semantics must be valid for the domain।


Practice 15 — Thread Safety

Does this become safe because it is inside CompletableFuture?

count++;

Answer

No।

Shared state still requires correct concurrency control।


True or False

  1. A Task and a Thread are the same abstraction.
  2. Runnable returns no value.
  3. Callable<T> can return a value.
  4. Future<T> may represent a result that is not ready yet.
  5. Future.get() never blocks.
  6. ExecutorService should have a lifecycle strategy.
  7. CompletableFuture.runAsync() produces a meaningful result value.
  8. supplyAsync() can produce a value.
  9. thenApply() transforms a completed value.
  10. thenCompose() is useful for chaining future-producing operations.
  11. thenCombine() is useful for independent computations whose results must be combined.
  12. Async execution automatically makes shared state thread-safe.
  13. Async execution always improves performance.
  14. Creating unlimited executors is harmless.
  15. Blocking at every asynchronous stage can reduce the benefit of asynchronous composition.

Answers

1. False
2. True
3. True
4. True
5. False
6. True
7. False
8. True
9. True
10. True
11. True
12. False
13. False
14. False
15. True

Knowledge Check

Question 1

Task এবং Thread-এর difference কী?

Question 2

Runnable এবং Callable<T>-এর difference কী?

Question 3

ExecutorService raw Thread creation-এর তুলনায় কী abstraction দেয়?

Question 4

Future<T> কী represent করে?

Question 5

Future.get() কেন blocking হতে পারে?

Question 6

Executor shutdown কেন important?

Question 7

runAsync() এবং supplyAsync()-এর difference কী?

Question 8

thenApply() কী করে?

Question 9

thenCompose() কেন প্রয়োজন?

Question 10

thenCombine() কখন useful?

Question 11

exceptionally() blindly ব্যবহার করা কেন dangerous?

Question 12

কেন immediately join() বা get() করা async benefit কমাতে পারে?

Question 13

Custom Executor কেন useful?

Question 14

Async code-এ thread safety কেন এখনো relevant?

Question 15

কেন more threads সবসময় more performance নয়?


Knowledge Check Answers

Answer 1

Task বলে:

কী কাজ করতে হবে।

Thread হলো execution resource/path যা সেই task execute করতে পারে।

Answer 2

Runnable:

() → void

কোনো result return করে না।

Callable<T>:

() → T

একটি value return করতে পারে এবং checked exception declare করতে পারে।

Answer 3

এটি task submission এবং execution-resource management আলাদা করে।

Application task submit করে, Executor execution strategy manage করে।

Answer 4

এটি future-এ complete হতে পারে এমন একটি computation/result represent করে।

Answer 5

যদি task এখনো complete না হয়, get() calling thread-কে result ready হওয়া পর্যন্ত wait করাতে পারে।

Answer 6

Executors execution resources own করতে পারে।

Shutdown না করলে application lifecycle/resource management incorrect হতে পারে।

Answer 7

runAsync() no-value asynchronous work-এর জন্য এবং:

CompletableFuture<Void>

return করে।

supplyAsync() value-producing asynchronous computation-এর জন্য:

CompletableFuture<T>

return করে।

Answer 8

Previous completed value-এর উপর synchronous transformation stage apply করে এবং transformed result-এর future দেয়।

Conceptually:

T → R

Answer 9

যদি next function নিজেই:

CompletableFuture<R>

return করে, thenCompose() nested future flatten করে।

Conceptually:

CompletableFuture<T>
→ function T → CompletableFuture<R>
→ CompletableFuture<R>

Answer 10

যখন দুইটি computations independentভাবে চলতে পারে এবং উভয়ের results combine করে final value দরকার।

Answer 11

সব exception fallback-এ convert করলে:

real failure
programming bug
infrastructure failure
expected absence

একই result হিসেবে hide হয়ে যেতে পারে।

Answer 12

কারণ caller প্রতিটি stage-এর পর wait করলে independent asynchronous execution/composition-এর সুযোগ কমে যায়।

Instead stages chain করা যায়।

Answer 13

Custom Executor resource usage, concurrency level, task isolation এবং lifecycle control করতে সাহায্য করে।

Answer 14

কারণ asynchronous tasks multiple threads-এ shared state access করতে পারে।

Race condition, visibility, atomicity-এর rules একই থাকে।

Answer 15

CPU এবং system resources finite।

Too many threads can create:

context switching
memory overhead
contention
queue pressure

More concurrency only useful when workload এবং resources support করে।


Practical API Selection Guide

Need no-result task:

Runnable

Need result-producing task:

Callable<T>

Need controlled task execution:

ExecutorService

Need eventual result:

Future<T>

Need asynchronous composition:

CompletableFuture<T>

Need async no-result work:

runAsync()

Need async result:

supplyAsync()

Need transform result:

thenApply()

Need consume result:

thenAccept()

Need run something after completion without using result:

thenRun()

Need async dependent operation:

thenCompose()

Need combine independent results:

thenCombine()

Need failure fallback:

exceptionally()

Need inspect success and failure:

handle()

CompletableFuture Mental Model

Think of:

CompletableFuture<Course>

as:

A Course computation
that may complete later
and can be connected
to more computation.

Example:

loadCourseAsync()
        .thenCompose(
                this::loadLessonsAsync
        )
        .thenApply(
                List::size
        )
        .thenAccept(
                System.out::println
        );

Read:

Course load করো
↓
তার Lessons asynchronously load করো
↓
lesson count বের করো
↓
count consume করো

No need to manually:

wait
extract
start next
wait again

between every stage।


Core Mental Model

Concurrency code design করার সময় তিনটি concept আলাদা রাখুন:

Task
→ কী কাজ?

Execution mechanism
→ কোথায়/কীভাবে run করবে?

Composition
→ result-এর পরে কী হবে?

For example:

CompletableFuture.supplyAsync(
        this::loadCourse,
        executor
)
        .thenApply(
                Course::title
        );

Here:

loadCourse
→ task

executor
→ execution mechanism

thenApply
→ composition

এই separation modern Java concurrency code বুঝতে খুব important।


Lesson Summary

এই lesson-এ আমরা higher-level Java concurrency APIs শিখেছি।

আমরা শিখেছি:

  • Task এবং Thread আলাদা concepts
  • Raw Thread creation সব application workload-এর best abstraction নয়
  • Runnable no-result task represent করে
  • Callable<T> result-producing task represent করে
  • ExecutorService task execution manage করে
  • Fixed thread pool bounded worker set use করতে পারে
  • execute() Runnable task execute করে
  • submit() Future return করতে পারে
  • Future<T> eventual result represent করে
  • Future.get() blocking হতে পারে
  • Task failure ExecutionException-এর মাধ্যমে surface হতে পারে
  • Interruption deliberately handle করা উচিত
  • Future cancellation cooperative
  • Executor lifecycle এবং shutdown important
  • CompletableFuture asynchronous computation compose করতে দেয়
  • runAsync() no-result asynchronous work
  • supplyAsync() result-producing asynchronous work
  • thenApply() result transform করে
  • thenAccept() result consume করে
  • thenRun() previous result ছাড়া follow-up work চালায়
  • thenCompose() dependent future-producing operations flatten করে
  • thenCombine() independent results combine করে
  • exceptionally() failure recovery করতে পারে
  • handle() success/failure দুইটি inspect করতে পারে
  • Custom Executor resource control দেয়
  • Immediately blocking at every stage async composition-এর benefit কমাতে পারে
  • Async code automatically thread-safe নয়
  • More threads automatically more performance দেয় না
  • CPU-bound এবং waiting workloads-এর characteristics আলাদা
  • Executor ownership এবং lifecycle clear হওয়া উচিত
  • Unbounded asynchronous fan-out resource pressure তৈরি করতে পারে
  • Async boundaries hidden thread-local context assumptions break করতে পারে

সবচেয়ে important principle:

Thread manage করার চেয়ে
Task এবং dependency model করা
সাধারণত বেশি useful abstraction।

আর CompletableFuture-এর core idea:

Wait manually less,
compose computation more.

Next Lesson

পরবর্তী lesson:

Virtual Threads and Concurrent Collections

আমরা শিখব:

  • Platform Thread এবং Virtual Thread-এর conceptual difference
  • Virtual Thread কেন useful
  • High-concurrency blocking workloads
  • Virtual Thread CPU-bound কাজকে magically faster করে না
  • Task-per-thread model
  • Thread.startVirtualThread(...)
  • Virtual-thread-per-task Executor
  • কেন Virtual Threads pool করা সাধারণত দরকার হয় না
  • Concurrency limit এবং resource limit-এর difference
  • ConcurrentHashMap
  • CopyOnWriteArrayList
  • BlockingQueue
  • Atomic collection operations
  • putIfAbsent()
  • computeIfAbsent()
  • Thread-safe collection বনাম thread-safe business workflow
  • Modern Java concurrency design-এর practical foundation