Modern Java
Executors, Callable, Future, and CompletableFuture
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 নয়
ExecutorServiceexecute()submit()RunnableCallable<T>Future<T>get()isDone()cancel()- Executor shutdown
CompletableFuturerunAsync()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
- A Task and a Thread are the same abstraction.
Runnablereturns no value.Callable<T>can return a value.Future<T>may represent a result that is not ready yet.Future.get()never blocks.ExecutorServiceshould have a lifecycle strategy.CompletableFuture.runAsync()produces a meaningful result value.supplyAsync()can produce a value.thenApply()transforms a completed value.thenCompose()is useful for chaining future-producing operations.thenCombine()is useful for independent computations whose results must be combined.- Async execution automatically makes shared state thread-safe.
- Async execution always improves performance.
- Creating unlimited executors is harmless.
- 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 নয়
Runnableno-result task represent করেCallable<T>result-producing task represent করেExecutorServicetask 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
CompletableFutureasynchronous computation compose করতে দেয়runAsync()no-result asynchronous worksupplyAsync()result-producing asynchronous workthenApply()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
ConcurrentHashMapCopyOnWriteArrayListBlockingQueue- Atomic collection operations
putIfAbsent()computeIfAbsent()- Thread-safe collection বনাম thread-safe business workflow
- Modern Java concurrency design-এর practical foundation