সরাসরি পর্বে যান
পর্ব ৪১ · ৫৯-এর মধ্যে · মডিউল ৬

Executor আর CompletableFuture — thread pool, Future আর অপেক্ষাহীন কাজের শিকল

Executors and CompletableFuture — Thread Pools, Futures, Coordination Tools and Non-blocking Pipelines

Taught in Bangla, this lesson moves from raw threads to thread pools: Executor, ExecutorService and Executors, Runnable versus Callable, Future and invokeAll, proper shutdown, sizing pools for CPU-bound and I/O-bound work, CountDownLatch, Semaphore, CyclicBarrier and BlockingQueue, and CompletableFuture pipelines with fan-out, fan-in, error handling, timeouts and scheduled tasks.

এই পর্বে যা শিখবেন

  • Thread pool কেন দরকার, আর Executor, ExecutorService, Executors, ThreadPoolExecutor-এর ভূমিকা
  • Runnable বনাম Callable, Future-এর জীবনচক্র, invokeAll/invokeAny আর সঠিকভাবে বন্ধ করা
  • Pool-এর আকার ঠিক করা: CPU-নির্ভর বনাম I/O-নির্ভর কাজ, Little-এর সূত্র আর Goetz-এর হিসাব
  • CountDownLatch, Semaphore, CyclicBarrier আর BlockingQueue দিয়ে thread-দের সমন্বয়
  • CompletableFuture-এর শিকল, fan-out/fan-in, ভুল সামলানো, timeout, নির্দিষ্ট সময় পরপর কাজ আর structured concurrency-র প্রথম ঝলক

১ · প্রতিটি কাজে নতুন thread কেন ভুল — thread pool-এর ধারণা

পর্ব ৪০-এ আপনি thread নিজে হাতে তৈরি করেছেন: new Thread(task).start(). সেখানে দেখেছেন race condition, synchronized আর thread-safe কোডের প্রয়োজন। এবার প্রশ্ন বদলায়: একটি অনলাইন দোকানের সার্ভারে প্রতি সেকেন্ডে কয়েকশো অনুরোধ আসে। প্রতিটির জন্য কি একটি করে নতুন thread?

একটি সুপারশপের কথা ভাবুন। প্রতিটি নতুন ক্রেতা এলে মালিক যদি একজন নতুন ক্যাশিয়ার নিয়োগ দেন — বিজ্ঞাপন, সাক্ষাৎকার, প্রশিক্ষণ, তারপর এক ক্রেতার বিল করে চাকরি শেষ — দোকান দেউলিয়া হতে বেশি দিন লাগবে না। বাস্তবে দোকানে থাকেন তিন-চারজন স্থায়ী ক্যাশিয়ার আর একটি লাইন। ক্রেতা লাইনে দাঁড়ান; যে কাউন্টার খালি হয়, পরের জন সেখানে যান। ভিড় বেশি হলে লাইন লম্বা হয়, কিন্তু ক্যাশিয়ারের সংখ্যা হুট করে হাজার হয়ে যায় না। এই ব্যবস্থার নাম thread pool: ক্যাশিয়ার হলো worker thread, লাইন হলো কাজের সারি (queue), আর ক্রেতা হলো আপনার জমা দেওয়া কাজ।

নতুন thread দামি কেন? Java-র সাধারণ platform thread আসলে অপারেটিং সিস্টেমের একটি thread. প্রতিটির জন্য OS-কে কাঠামো বানাতে হয়, আর সাধারণত ১ MB পর্যন্ত stack-এর জায়গা সংরক্ষিত থাকে। হাজার হাজার thread একসাথে থাকলে CPU-কে বারবার এক thread থামিয়ে আরেকটি চালাতে হয় — এই context switch-এ সময় যায় আসল কাজের বদলে। আর সীমা না থাকলে একটি ভিড়ের মুহূর্তেই মেমরি ফুরিয়ে OutOfMemoryError: unable to create native thread-এর মতো ভুল আসে। Pool এই তিন সমস্যাই সামলায়: thread একবার তৈরি হয়ে বারবার ব্যবহার হয়, সংখ্যা সীমিত থাকে, আর বাড়তি কাজ সারিতে অপেক্ষা করে।

1var pool = Executors.newFixedThreadPool(3);2for (Task t : tasks)        // ৬টি কাজ3    pool.submit(t);4// worker: সারি থেকে নাও → চালাও → আবার নাও5pool.close();               // সব শেষের অপেক্ষা
এই মুহূর্তে
Worker-1
Worker-2
Worker-3
সারি
  1. Pool তৈরি। তিনজন worker-এর জায়গা ঠিক হলো। এখনো কোনো কাজ নেই, সারি খালি। আসল thread তৈরি হয় প্রথম কাজ আসার সময়।
  2. ৬টি কাজ জমা। submit কাজ চালায় না, শুধু জমা দেয় আর সাথে সাথে ফিরে আসে। প্রথম তিনটি কাজ তিন worker পায়; বাকি T4, T5, T6 সারিতে দাঁড়ায়। মূল thread একটুও আটকায়নি।
  3. T2 আগে শেষ। কাজগুলোর দৈর্ঘ্য সমান নয়। Worker-2 খালি হয়েই সারির মাথা থেকে T4 তুলে নিল। কেউ তাকে বলে দেয়নি — worker-এর লুপই এটা: নাও, চালাও, আবার নাও।
  4. T1 শেষ, T5 শুরু। জমার ক্রম আর শেষ হওয়ার ক্রম আলাদা। তাই concurrent কাজের শেষ হওয়ার ক্রম কখনো ছাপার ভরসা করবেন না — ফল নেবেন জমার ক্রমে, পরের অংশে যেমন দেখবেন।
  5. সারি খালি। Worker-3 শেষ কাজ T6 নিল। এখন তিনটি কাজ চলছে, অপেক্ষায় কেউ নেই।
  6. খালি worker মারা যায় না। T4 শেষে Worker-2 নতুন কাজের জন্য সারিতে অপেক্ষা করে। এই অপেক্ষমাণ thread-গুলোর জন্যই pool বন্ধ না করলে প্রোগ্রাম কখনো শেষ হয় না।
  7. close(). নতুন কাজ নেওয়া বন্ধ, চলমান কাজ শেষ হওয়া পর্যন্ত অপেক্ষা, তারপর worker-রা বিদায় নেয়। ছয়টি কাজ, কিন্তু thread তৈরি হয়েছে মাত্র তিনটি।

Java 1.0 (১৯৯৬) থেকেই Thread ছিল, কিন্তু pool, সারি বা ফল ফেরত পাওয়ার কোনো মানসম্মত ব্যবস্থা ছিল না — প্রতিটি দল নিজের মতো বানাত, আর প্রায়ই ভুল করত। ২০০৪ সালে Java 5-এ Doug Lea-র নেতৃত্বে JSR 166 যোগ করে java.util.concurrent প্যাকেজ: Executor, Future, BlockingQueue, CountDownLatch, Semaphore সব এক সাথে। Java 7-এ আসে Fork/Join, Java 8-এ CompletableFuture, আর Java 21-এ virtual thread. ভাবনার প্রশ্ন ১-এ বিস্তারিত।

২ · Executor, ExecutorService আর Executors-এর কারখানা

নামগুলো কাছাকাছি, তাই শুরুতেই আলাদা করে নিন:

Executor

সবচেয়ে ছোট interface — Executor: একটিমাত্র method execute(Runnable). অর্থ — “এই কাজটা কোথাও চালাও”। কোন thread-এ, কখন, তা লুকানো।

“কী চালাব” আর “কীভাবে চালাব” আলাদা

ExecutorService

ExecutorService হলো Executor-এর বড় রূপ: submit (ফেরত দেয় Future), invokeAll, invokeAny, shutdown, awaitTermination, close.

প্রতিদিন এটাই ব্যবহার করবেন

Executors

শেষে “s” — একটি utility class, যার static method তৈরি করা pool দেয়: Executors.newFixedThreadPool(4).

কারখানা (factory)
Methodকী পানকখন / সাবধানতা
newFixedThreadPool(n)ঠিক n-টি thread, সীমাহীন সারিসবচেয়ে প্রচলিত। কিন্তু সারির সীমা নেই — কাজ আসার গতি বেশি হলে সারি বাড়তেই থাকে, মেমরি ফুরায়।
newSingleThreadExecutor()একটি thread; কাজ চলে জমার ক্রমে, একটির পর একটিলগ ফাইলে লেখা, একটি ডিভাইসের সাথে কথা বলা — যেখানে ক্রম জরুরি আর একসাথে দুটি চলা চলবে না।
newCachedThreadPool()দরকার হলেই নতুন thread, খালি thread ৬০ সেকেন্ড পরে বিদায়বিপজ্জনক। thread-এর কোনো উপরের সীমা নেই। ছোট, দ্রুত কাজে ভালো; কিন্তু ধীর কাজের ঢেউ এলে হাজার হাজার thread তৈরি হয়ে সার্ভার বসে যায়।
newScheduledThreadPool(n)দেরিতে বা নির্দিষ্ট সময় পরপর কাজ চালানোপ্রতি ৩০ সেকেন্ডে পেমেন্টের অবস্থা দেখা, রাত ২টায় রিপোর্ট — অংশ ৮-এ।
newVirtualThreadPerTaskExecutor()প্রতিটি কাজে একটি হালকা virtual thread (Java 21)প্রচুর I/O-অপেক্ষার কাজ — পর্ব ৪২-এর বিষয়।

ভেতরের যন্ত্র — ThreadPoolExecutor

প্রথম তিনটি factory আসলে একই class-এর ভিন্ন সেটিং: ThreadPoolExecutor. বড় প্রজেক্টে অনেকে factory বাদ দিয়ে এটাই সরাসরি তৈরি করেন, যাতে প্রতিটি সীমা চোখের সামনে থাকে:

ExecutorService pool = new ThreadPoolExecutor(
    4, 8,                                   // core: সবসময় ৪টি, max: সর্বোচ্চ ৮টি
    30, TimeUnit.SECONDS,                   // বাড়তি thread কতক্ষণ খালি বসে থাকবে
    new ArrayBlockingQueue<>(100),          // সারিতে সর্বোচ্চ ১০০টি কাজ
    new ThreadPoolExecutor.CallerRunsPolicy());   // সব ভরা হলে কী হবে

এখানে একটি ক্রম অনেককে অবাক করে। নতুন কাজ এলে: (১) core-এর চেয়ে কম thread চললে নতুন thread; (২) নইলে সারিতে; (৩) সারি ভরা হলে তবেই max পর্যন্ত বাড়তি thread; (৪) তাও ভরা হলে কাজটি প্রত্যাখ্যাত। অর্থাৎ সীমাহীন সারি দিলে max কখনো কাজেই লাগে না। প্রত্যাখ্যানের নিয়মকে বলে rejection policy:

  • AbortPolicy (ডিফল্ট) — RejectedExecutionException ছোড়ে। বন্ধ করা pool-এ submit করলেও এটাই আসে।
  • CallerRunsPolicy — যে thread জমা দিচ্ছিল, সে নিজেই কাজটা চালায়। ফলে জমা দেওয়ার গতি নিজে থেকেই কমে — চাপ সামলানোর সহজ উপায়।
  • DiscardPolicy / DiscardOldestPolicy — নতুন বা সবচেয়ে পুরোনো কাজটি চুপচাপ ফেলে দেয়। শুধু তখনই, যখন কাজ হারালে ক্ষতি নেই (যেমন লাইভ স্কোরের পুরোনো আপডেট)।

৩ · Runnable বনাম Callable, আর Future — ফলের রসিদ

কাজ দুই ধরনের। Runnable-এর method void run() — কিছু ফেরত দেয় না, checked exception ছুড়তে পারে না। Callable<V>-এর method V call() throws Exception — একটি ফল দেয়, আর যেকোনো exception ছুড়তে পারে। দুটিই পর্ব ৩৪-এর functional interface, তাই lambda দিয়ে লেখা যায়। Lambda কিছু ফেরত দিলে submit তাকে Callable ধরে নেয়; কিছু ফেরত না দিলে সেটি Runnable, আর তখন ভেতরে checked exception ছোড়া যায় না।

Runnable-এর ভেতরে checked exception ছুড়লে compiler থামিয়ে দেয়:

class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            Thread.sleep(100);
        };
    }
}
Main.java:4: error: unreported exception InterruptedException; must be caught or declared to be thrown
            Thread.sleep(100);
                        ^
1 error

পর্ব ২৯-এর checked exception-এর নিয়ম: run() কিছু ঘোষণা করে না, তাই ভেতরে try-catch লাগবে। একই lambda Callable হলে (যেমন () -> { Thread.sleep(100); return 1; }) কোনো সমস্যা নেই।

Future — যে ফল পরে আসবে, তার রসিদ

দর্জির দোকানে জামা বানাতে দিলে দর্জি একটি রসিদ দেন। জামা তখনই হয় না, কিন্তু রসিদ হাতে আপনি অন্য কাজে যেতে পারেন। পরে রসিদ দেখিয়ে জামা নেবেন — তৈরি না হলে দোকানে বসে অপেক্ষা। pool.submit(callable) ঠিক তেমনি সাথে সাথে একটি Future<V> দেয়। এর method:

  • get() — ফল না আসা পর্যন্ত বর্তমান thread আটকে থাকে (blocking), তারপর ফল দেয়।
  • get(2, TimeUnit.SECONDS) — সর্বোচ্চ দুই সেকেন্ড অপেক্ষা; তার মধ্যে না এলে java.util.concurrent.TimeoutException (কোনো বার্তা ছাড়া)। কাজটি কিন্তু থামে না, চলতেই থাকে।
  • isDone() — অপেক্ষা না করে জানায় শেষ হয়েছে কি না (সফল, ব্যর্থ বা বাতিল — তিনটিতেই true).
  • cancel(true) — বাতিল; শুরু না হলে কখনো চলবে না, চলমান হলে thread-কে interrupt পাঠায়। এরপর get() ছোড়ে CancellationException.
  1. submitFuture fসাথে সাথে রসিদ
  2. সারিতেisDone() falseworker নেই
  3. চলছেcall()worker thread-এ
  4. শেষisDone() trueফল বা exception জমা
  5. get()120ফল হাতে
ধাপ ১জমা দেওয়া
Future<Integer> f = pool.submit(() -> countRows("jan.csv"));
submit ফিরে এল সাথে সাথে — কাজ এখনো শুরুই হয়নি
মূল thread পরের লাইনে চলে যেতে পারে
ধাপ ২সারিতে অপেক্ষা
f.isDone()     → false
f.cancel(false) → true হতো, আর কাজটি কখনো চলত না
এখন f.get() ডাকলে মূল thread আটকে যাবে
ধাপ ৩worker চালাচ্ছে
Worker-2: call() → ফাইল পড়া চলছে …
f.isDone() → false
cancel(true) দিলে Worker-2-কে interrupt পাঠানো হতো
ধাপ ৪শেষ — ফল জমা হলো
call() ফেরত দিল 120
Future-এর ভেতরে রাখা: 120
f.isDone() → true
ধাপ ৫ফল নেওয়া
int n = f.get();   → 120, কোনো অপেক্ষা ছাড়াই
কাজ শেষ হওয়ার আগে ডাকলে এই লাইনেই অপেক্ষা করত
ধাপ ৬যদি call() exception ছুড়ত
call() ছুড়ল IllegalStateException("payment failed")
Future সেটা জমা রাখে — কেউ get() না ডাকলে কেউ জানবেও না!
f.get() → ExecutionException
  e.getCause() → IllegalStateException: payment failed
  1. রসিদ হাতে। submit কাজ চালায় না, শুধু সারিতে রাখে আর একটি Future ফেরত দেয়। এই মুহূর্তে কাজটি শুরু হয়েছে কি না, আপনি জানেন না — জানার দরকারও নেই।
  2. সারিতে। সব worker ব্যস্ত, কাজ অপেক্ষায়। এখনই বাতিল করলে কাজটি কখনো চলবে না। এখন get() ডাকা মানে দোকানে বসে থাকা।
  3. চলছে। একজন worker কাজটি তুলে call() চালাচ্ছে। মূল thread ততক্ষণ অন্য কাজ জমা দিতে বা অন্য হিসাব করতে পারে — এটাই সমান্তরালতার লাভ।
  4. শেষ। ফলটি Future-এর ভেতরে রাখা হলো। isDone() এখন true. Future একবারই শেষ হয়; ফল আর বদলায় না।
  5. get() ফল দেয়। শেষ হয়ে থাকলে সাথে সাথে, না হলে শেষ হওয়া পর্যন্ত অপেক্ষার পর। একাধিকবার ডাকলেও একই ফল।
  6. ব্যর্থতার পথ। কাজ exception ছুড়লে সেটা হারায় না, Future-এ জমা থাকে। get() সেটাকে ExecutionException-এ মুড়ে ছোড়ে; আসল কারণ পাবেন getCause()-এ। বিপদ হলো: কেউ get() না ডাকলে ভুলটা নীরবে হারিয়ে যায়।

মোড়ানো exception না ধরলে stack trace দেখতে এমন — দুই অংশে:

import java.util.concurrent.*;

class Main {
    public static void main(String[] args) throws Exception {
        try (ExecutorService pool = Executors.newFixedThreadPool(2)) {
            Future<Integer> f = pool.submit(() -> 100 / 0);
            System.out.println(f.get());
        }
    }
}
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
	at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
	at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
	at Main.main(Main.java:7)
Caused by: java.lang.ArithmeticException: / by zero
	at Main.lambda$main$0(Main.java:6)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	at java.base/java.lang.Thread.run(Thread.java:1583)

প্রথম অংশ বলছে কোথায় ধরা পড়ল — মূল thread-এ, লাইন ৭-এর get()-এ। Caused by অংশ বলছে কোথায় ঘটেছিল — worker thread-এ, লাইন ৬-এর lambda-য়, ThreadPoolExecutor.runWorker-এর ভেতর দিয়ে। পর্ব ২৯-এর exception chaining-এর বাস্তব রূপ। Debug করার সময় সবসময় Caused by পর্যন্ত পড়ুন।

৪ · অনেক কাজ একসাথে, সঠিকভাবে বন্ধ, আর pool কত বড়

একটি রিপোর্ট জেনারেটর ভাবুন: চারটি মাসিক CSV ফাইল, প্রতিটি পড়তে সময় লাগে, আর শেষে মোট সারির সংখ্যা চাই। একটি ফাইল নেই — সেই ভুল যেন পুরো রিপোর্ট না থামায়। নিয়ম দুটি: আগে সব কাজ জমা দিন, তারপর ফল নিন; আর ফল নিন জমার ক্রমে, যাতে আউটপুট প্রতিবার একই থাকে।

import java.util.*;
import java.util.concurrent.*;

class Main {
    static final Map<String, Integer> ROWS = Map.of("jan.csv", 120, "feb.csv", 95, "mar.csv", 140);

    static int countRows(String file) throws InterruptedException {
        Thread.sleep(100);                                   // ধীর ফাইল পড়ার ভান
        Integer rows = ROWS.get(file);
        if (rows == null) throw new IllegalStateException("ফাইল পাওয়া যায়নি: " + file);
        return rows;
    }
    public static void main(String[] args) throws InterruptedException {
        List<String> files = List.of("jan.csv", "feb.csv", "apr.csv", "mar.csv");
        int total = 0;
        try (ExecutorService pool = Executors.newFixedThreadPool(3)) {
            List<Future<Integer>> receipts = new ArrayList<>();
            for (String f : files) receipts.add(pool.submit(() -> countRows(f)));  // সব আগে জমা
            for (int i = 0; i < files.size(); i++) {
                try {
                    int n = receipts.get(i).get();                // জমার ক্রমেই ফল নেওয়া
                    total += n;
                    System.out.println(files.get(i) + " -> " + n);
                } catch (ExecutionException e) {
                    System.out.println(files.get(i) + " -> ব্যর্থ: " + e.getCause().getMessage());
                }
            }
        }                                                         // close(): সব কাজ শেষের অপেক্ষা
        System.out.println("মোট সারি: " + total);
    }
}

আউটপুট প্রতিবার একই: jan.csv -> 120, feb.csv -> 95, apr.csv -> ব্যর্থ: ফাইল পাওয়া যায়নি: apr.csv, mar.csv -> 140, শেষে মোট সারি: 355. কাজগুলো তিনটি worker-এ একসাথে চলেছে, কিন্তু কোনটা আগে শেষ হলো তা আমরা ছাপিনি — receipts তালিকা জমার ক্রমে সাজানো, আর get() সেই ক্রমেই ডাকা হয়েছে। Lambda countRows ডাকে, যা checked InterruptedException ছোড়ে আর একটি ফল দেয়, তাই এটি Callable. একটি ফাইলের ব্যর্থতা ExecutionException হয়ে শুধু সেই সারিতে ধরা পড়েছে; বাকি রিপোর্ট চলেছে। e.getMessage() ছাপলে আসত পুরো java.lang.IllegalStateException: …, তাই আসল বার্তার জন্য getCause(). চারটি ১০০ ms-এর কাজ একে একে চললে লাগত প্রায় ৪০০ ms; তিন worker-এ লাগে মোটামুটি দুই ধাপের সময়। (অনলাইন runner ফাইলটিকে অন্য নামে compile করে, তাই Main এখানে public নয়।)

সবচেয়ে সাধারণ ভুল — জমা দিয়েই অপেক্ষা। for (String f : files) total += pool.submit(() -> countRows(f)).get(); দেখতে ছোট, কিন্তু প্রতিটি get() পরের কাজ জমা দেওয়ার আগেই অপেক্ষা করে। ফলে কাজগুলো একটির পর একটি চলে — pool থাকা সত্ত্বেও সমান্তরালতা শূন্য, উল্টো বাড়তি খরচ।

invokeAll আর invokeAny

ওপরের “সব জমা, তারপর সব ফল” ধাঁচটি এত প্রচলিত যে এর জন্য তৈরি method আছে। invokeAll(tasks) সব কাজ জমা দেয়, সবগুলো শেষ হওয়া পর্যন্ত অপেক্ষা করে, আর Future-এর তালিকা দেয় — কাজের তালিকার ঠিক একই ক্রমে, কে আগে শেষ হলো তা নির্বিশেষে। invokeAny(tasks) সব জমা দেয়, যেটি প্রথম সফলভাবে শেষ হয় তার ফল দেয়, আর বাকিগুলো বাতিল করে। যেমন একই তথ্য তিনটি mirror সার্ভারে চাওয়া — যে আগে দেয়। (এর ফল কোন কাজের, তা সময়ের ওপর নির্ভর করে, তাই টেস্টে ছাপার আগে ভাবুন।)

বন্ধ করা — shutdown, shutdownNow, awaitTermination, close

Pool-এর worker-রা সাধারণ thread — daemon thread নয় — আর খালি থাকলে তারা নতুন কাজের অপেক্ষায় বসে থাকে। JVM তখনই শেষ হয়, যখন সব non-daemon thread শেষ — তাই pool বন্ধ না করলে main শেষ হলেও প্রোগ্রাম চলতেই থাকে, কিছু ছাপে না, শুধু ঝুলে থাকে। বন্ধ করার চারটি উপায়:

Methodনতুন কাজসারির কাজচলমান কাজঅপেক্ষা করে?
shutdown()নেয় নাচলবেশেষ হবেনা — সাথে সাথে ফেরে
shutdownNow()নেয় নাবাদ; শুরু না হওয়া কাজের তালিকা ফেরত দেয়interrupt পাঠায়না
awaitTermination(t, unit)কিছু বদলায় না — শুধু সব শেষ হওয়া পর্যন্ত (সর্বোচ্চ t সময়) অপেক্ষা; শেষ হলে trueহ্যাঁ
close() (Java 19 থেকে)নেয় নাচলবেশেষ হবেহ্যাঁ, সব শেষ হওয়া পর্যন্ত

Java 19 থেকে ExecutorService পর্ব ২৯-এর AutoCloseable, তাই try-with-resources-এ রাখা যায় — ওপরের দুটি প্রোগ্রাম তা-ই করেছে। Java 21-এ এটাই সবচেয়ে নিরাপদ অভ্যাস। পুরোনো কোডে দেখবেন হাতে লেখা ধাঁচ: shutdown(), তারপর awaitTermination(30, SECONDS), সময়মতো শেষ না হলে shutdownNow(). একটি বাস্তব উদাহরণ — অর্ডার পাঠানোর পর হাজার ক্রেতাকে SMS:

try (ExecutorService smsPool = Executors.newFixedThreadPool(10)) {
    for (Customer c : customers)
        smsPool.execute(() -> smsGateway.send(c.phone(), "আপনার অর্ডার পাঠানো হয়েছে"));
}   // এখানে পৌঁছালে সব SMS-এর চেষ্টা শেষ

এখানে submit নয়, execute — কারণ ফল দরকার নেই। পার্থক্যটা জরুরি: submit-এর কাজ exception ছুড়লে সেটা Future-এ জমা থাকে, আর কেউ get() না ডাকলে কোথাও কিছু দেখা যায় না। execute-এর কাজের uncaught exception অন্তত stderr-এ stack trace হিসেবে আসে। আরও ভালো: কাজের ভেতরেই try-catch দিয়ে ব্যর্থ নম্বরগুলো লগ করুন, যাতে পরে আবার পাঠানো যায়। আর gateway যদি সেকেন্ডে ২০টির বেশি SMS নিতে না দেয়? Pool-এর আকারই তখন একটি সীমা — ১০টি thread মানে একসাথে সর্বোচ্চ ১০টি অনুরোধ।

Pool কত বড় হবে?

  • CPU-নির্ভর কাজ (ছবি resize, হিসাব, কম্প্রেশন): thread সারাক্ষণ CPU ব্যবহার করে। Core-এর চেয়ে বেশি thread দিলে লাভ নেই, শুধু context switch. আকার ≈ core সংখ্যা: Runtime.getRuntime().availableProcessors().
  • I/O-নির্ভর কাজ (ডেটাবেস, HTTP, ফাইল): thread বেশিরভাগ সময় উত্তরের অপেক্ষায় বসে থাকে, CPU খালি। তাই core-এর চেয়ে অনেক বেশি thread লাগে।

কত বেশি? Brian Goetz-এর Java Concurrency in Practice (২০০৬)-এর হিসাব: thread ≈ core × (১ + অপেক্ষার সময় ÷ হিসাবের সময়). ৮ core, প্রতিটি অনুরোধে ৯০ ms ডেটাবেসের অপেক্ষা আর ১০ ms হিসাব হলে ৮ × (১ + ৯) = ৮০. অন্য দিক থেকে পর্ব ৩২-এর Little-এর সূত্র L = λW: সেকেন্ডে ৪০০ অনুরোধ, প্রতিটি ০.১ সেকেন্ড থাকে, তাহলে গড়ে ৪০টি অনুরোধ একসাথে ভেতরে — প্রতিটি একটি thread আটকে রাখলে অন্তত ৪০টি thread লাগবে। দুটোই শুরুর অনুমান; আসল সংখ্যা ঠিক হয় load test করে। আর অপেক্ষা যখন খুব বেশি — হাজার হাজার একসাথে HTTP কল — তখন platform thread-এর pool নয়, পর্ব ৪২-এর virtual thread.

৫ · thread-দের সমন্বয় — CountDownLatch, Semaphore, CyclicBarrier, BlockingQueue

কখনো ফল নয়, শৃঙ্খলা দরকার: “তিনটি সার্ভিস চালু না হওয়া পর্যন্ত অপেক্ষা করো”, “একসাথে তিনটির বেশি ডাউনলোড নয়”। java.util.concurrent-এ এর জন্য তৈরি হাতিয়ার আছে, যাতে wait/notify হাতে লিখতে না হয়।

CountDownLatch — গোনা শূন্য হলে দরজা খোলে

CountDownLatch একটি সংখ্যা দিয়ে শুরু হয়। প্রতিটি কাজ শেষে countDown() সংখ্যাটি এক কমায়; await() সংখ্যা শূন্য না হওয়া পর্যন্ত অপেক্ষা করে। একবার শূন্য হলে আর ফেরানো যায় না — একবারের দরজা।

CountDownLatch ready = new CountDownLatch(3);
for (String service : List.of("database", "cache", "payment"))
    pool.execute(() -> { connect(service); ready.countDown(); });
ready.await();                       // তিনটিই চালু হলে তবেই সামনে
System.out.println("অ্যাপ প্রস্তুত");

বাস্তবে connect ব্যর্থ হলেও যেন গোনা কমে, তাই countDown() রাখুন finally-তে, আর await(10, TimeUnit.SECONDS) দিয়ে সময়সীমা দিন — নইলে একটি ব্যর্থতায় অ্যাপ চিরকাল আটকে থাকবে।

Semaphore — একসাথে সর্বোচ্চ কয়টি

একটি বাইরের API বলেছে: একসাথে তিনটির বেশি অনুরোধ পাঠালে আপনাকে আটকে দেবে। আপনার pool-এ thread অনেক, কারণ অন্য কাজও আছে। Semaphore এখানে টিকিট-ব্যবস্থা: শুরুতে ৩টি টিকিট (permit). acquire() একটি টিকিট নেয় — না থাকলে অপেক্ষা; release() টিকিট ফেরত দেয়।

1Semaphore gate = new Semaphore(3);2gate.acquire();          // টিকিট নাও, না থাকলে অপেক্ষা3try { download(url); }4finally { gate.release(); }   // টিকিট ফেরত
এই মুহূর্তে
খালি টিকিট
ডাউনলোড চলছে
অপেক্ষায়
  1. ৩টি টিকিট। Semaphore তৈরি হলো ৩টি permit নিয়ে। পাঁচটি ডাউনলোডের কাজ জমা হয়েছে, আর pool-এ পাঁচটি thread — অর্থাৎ thread-এর দিক থেকে কোনো বাধা নেই।
  2. প্রথম তিনজন ঢুকল। D1, D2, D3 টিকিট নিল; খালি টিকিট শূন্য। D4 আর D5-এর thread চলছে ঠিকই, কিন্তু acquire()-এ অপেক্ষা করছে — API-তে একটি অনুরোধও যায়নি।
  3. একজন বেরোল, একজন ঢুকল। D2 শেষ হয়ে finally-তে টিকিট ফেরত দিল, আর অপেক্ষায় থাকা D4 সেটা পেল। release() finally-তে না থাকলে ডাউনলোড ব্যর্থ হলে টিকিট চিরতরে হারাত।
  4. আবার একই। D1 শেষ, D5 ঢুকল। অপেক্ষার সারি খালি। এখনো ভেতরে ঠিক তিনটি।
  5. টিকিট জমছে। D3 আর D4 শেষ; দুটি টিকিট খালি পড়ে আছে, কারণ নতুন কেউ চায়নি।
  6. সব শেষ। তিনটি টিকিটই ফেরত। Pool-এ পাঁচটি thread থাকলেও API একসাথে তিনটির বেশি অনুরোধ দেখেনি — Semaphore সীমা দেয় কাজের অংশে, pool দেয় thread-এ।

CyclicBarrier আর BlockingQueue — সংক্ষেপে

CyclicBarrier হলো মিলনস্থল: n-টি thread প্রত্যেকে await() ডাকে, আর সবাই না পৌঁছানো পর্যন্ত কেউ এগোয় না। তারপর বাধা আবার শুরুতে ফেরে — “cyclic”, বারবার ব্যবহার করা যায়। যেমন একটি simulation-এর প্রতিটি ধাপে চারটি thread মানচিত্রের চার অংশ হিসাব করে; সবাই শেষ করলে তবেই পরের ধাপ। আর পর্ব ৩২-এর BlockingQueue দিয়ে producer-consumer: একদল thread অর্ডার put করে, আরেকদল take করে প্রক্রিয়া করে; সারি ভরা হলে producer, খালি হলে consumer নিজে থেকেই অপেক্ষা করে। আসলে প্রতিটি ThreadPoolExecutor-এর ভেতরে ঠিক এই জিনিসটিই আছে — worker-রা হলো consumer, আর আপনার submit হলো producer.

৬ · CompletableFuture — “এটা শেষ হলে ওটা করো”

সাধারণ Future-এর বড় দুর্বলতা: ফল দিয়ে কিছু করতে হলে কাউকে get() ডেকে অপেক্ষা করতেই হয়। “দাম এলে ছাড় হিসাব করো, তারপর পেজ বানাও” — এই বাক্যটি Future দিয়ে লেখা যায় না, অপেক্ষা ছাড়া। Java 8-এর CompletableFuture ঠিক এটাই দেয়: আপনি আগে থেকেই শিকল বর্ণনা করেন, আর প্রতিটি ধাপ আগেরটি শেষ হলে নিজে থেকেই চলে। কোনো thread বসে অপেক্ষা করে না।

CompletableFuture<Void> done = CompletableFuture
    .supplyAsync(() -> priceService.get(productId), io)   // ব্যাকগ্রাউন্ডে শুরু
    .thenApply(price -> price * 90 / 100)                  // ফল এলে: ১০% ছাড়
    .thenAccept(p -> System.out.println("দাম: " + p));     // শেষ ধাপ: ব্যবহার
  • supplyAsync(supplier, executor) — একটি Supplier ব্যাকগ্রাউন্ডে চালায়, সাথে সাথে CompletableFuture দেয়। ফল না লাগলে runAsync(runnable).
  • thenApply(fn) — ফলকে রূপান্তর; পর্ব ৩৫-এর stream-এর map-এর মতো।
  • thenAccept(consumer) — ফল ব্যবহার করে, কিছু ফেরত দেয় না (CompletableFuture<Void>).
  • join() — শেষ পর্যন্ত অপেক্ষা করে ফল দেয়; get()-এর মতোই, কিন্তু checked exception ছোড়ে না — ভুল আসে unchecked CompletionException-এ মোড়ানো। get() ছোড়ে checked ExecutionException. শিকলের একদম শেষে, সাধারণত একবারই।

thenCompose বনাম thenCombine

দুটি সার্ভিস কল কীভাবে সম্পর্কিত, তার ওপর method নির্ভর করে:

নির্ভরশীল: thenCompose

দ্বিতীয় কলের জন্য প্রথমটির ফল লাগে — আগে ব্যবহারকারী খুঁজুন, তারপর তার শেষ অর্ডার। findUser(id).thenCompose(u -> lastOrder(u)). lastOrder নিজেই CompletableFuture দেয়; thenApply দিলে পেতেন বাক্সে বাক্স CompletableFuture<CompletableFuture<Order>>. thenCompose সেটা সমতল করে — stream-এর flatMap-এর মতো।

একটির পর একটি

স্বাধীন: thenCombine

দুটি কল একে অপরের ওপর নির্ভর করে না — দাম আর স্টক। দুটোই একসাথে শুরু করুন, দুটো এলে জোড়া দিন: price.thenCombine(stock, (p, s) -> …). thenCombine-এ মোট সময় হয় ধীরতমটির সমান, যোগফল নয়।

একসাথে, তারপর মিলন
supplyAsync(price)২০০ ms
supplyAsync(stock)১০০ ms
thenCombine(p, s) → জোড়া
thenApply(format)পেজের লেখা
join()ফল হাতে
ধাপ ১দুটি কাজ শুরু (সময় ০ ms)
var price = supplyAsync(() -> priceService(), io);
var stock = supplyAsync(() -> stockService(), io);
দুটি লাইনই সাথে সাথে ফিরে এল — দুটি worker কাজ শুরু করল
ধাপ ২শিকল বর্ণনা (এখনো ০ ms)
var page = price.thenCombine(stock, (p, s) -> …)
                .thenApply(Main::format);
এটা শুধু পরিকল্পনা — কোনো ফল আসেনি, কোনো thread অপেক্ষা করছে না
ধাপ ৩দুটো ফলই এল (~২০০ ms)
১০০ ms: stock = 7     (একা এসে অপেক্ষা)
২০০ ms: price = 1250
দুটো আসামাত্র thenCombine চলল → (1250, 7)
ধাপ ৪রূপান্তর
thenApply(format) → "দাম 1250 টাকা, স্টকে 7টি"
যে thread আগের ধাপ শেষ করেছে, সাধারণত সে-ই এটা চালায়
ধাপ ৫শেষে একবার join()
String html = page.join();
মোট সময় ≈ ২০০ ms (ধীরতম কল), ৩০০ ms নয়
  1. দুটি স্বাধীন কল শুরু। supplyAsync কাজ জমা দিয়েই ফেরে। দাম আর স্টক দুটি আলাদা worker-এ একসাথে চলছে। মূল thread মুক্ত।
  2. শিকলটা আগেই লেখা। thenCombine আর thenApply শুধু বলে রাখে: “দুটো এলে জোড়া দিও, তারপর সাজিও।” এটা callback নিবন্ধন, অপেক্ষা নয়।
  3. মিলন। স্টক আগে এসে অপেক্ষা করে; দাম আসামাত্র combine-এর function চলে। কে আগে এল তাতে ফল বদলায় না — function সবসময় (p, s) ক্রমে দুটি মান পায়।
  4. রূপান্তর। জোড়া দেওয়া মান থেকে পেজের লেখা তৈরি। প্রতিটি ধাপ একটি নতুন CompletableFuture, তাই শিকল যত লম্বাই হোক, কেউ থেমে নেই।
  5. শেষে join(). শুধু একদম শেষে, যেখানে সত্যিই ফল দরকার (যেমন HTTP উত্তর পাঠানো), একবার অপেক্ষা। মোট সময় দুটি কলের যোগফল নয়, ধীরতমটির সমান।

অনেকগুলো একসাথে — allOf আর anyOf

CompletableFuture.allOf(a, b, c) একটি CompletableFuture<Void> দেয়, যা সবগুলো শেষ হলে শেষ হয়। এর নিজের কোনো ফল নেই (join() দেয় null); ফল নিতে হয় প্রতিটি থেকে আলাদাভাবে, ক্রমমতো: allOf(a, b, c).thenApply(v -> List.of(a.join(), b.join(), c.join())) — এখানে join() আর অপেক্ষা করে না, কারণ সব শেষ। anyOf(...) দেয় যেটি প্রথম শেষ হয় তার ফল, টাইপ Object.

কোন thread-এ চলবে? Executor না দিলে supplyAsync চলে ForkJoinPool.commonPool()-এ — পুরো JVM-এর ভাগাভাগি করা একটি pool, যার আকার সাধারণত core সংখ্যার চেয়ে একটি কম। পর্ব ৩৫-এর parallel stream-ও এটাই ব্যবহার করে (এর নিচে Fork/Join আর work stealing). এখানে ধীর ডেটাবেস বা HTTP কল দিলে কয়েকটি কলেই pool-এর সব thread অপেক্ষায় আটকে যায়, আর প্রোগ্রামের অন্য সব async কাজ ও parallel stream থেমে থাকে। নিয়ম: ব্লক করে এমন I/O-র জন্য সবসময় নিজের executor দিন — supplyAsync(task, ioPool).

৭ · ভুল, timeout আর fan-out/fan-in — একটি পণ্যের পেজ

একটি ই-কমার্স পণ্যের পেজে চারটি তথ্য লাগে, চারটি আলাদা সার্ভিস থেকে: দাম, স্টক, ডেলিভারির সময়, রেটিং। একটির পর একটি ডাকলে সময় যোগ হয়। একসাথে ছড়িয়ে দেওয়া (fan-out), তারপর এক জায়গায় জোড়া (fan-in) — এই fan-out/fan-in ধাঁচে সময় হয় ধীরতম কলের সমান। কিন্তু বাস্তবের দুটি ঝামেলা: কোনো সার্ভিস খুব ধীর, কোনোটি ভেঙে পড়েছে। পেজ তবুও দেখাতে হবে।

  • exceptionally(ex -> বিকল্প) — আগের ধাপ ব্যর্থ হলে একটি বিকল্প মান দেয়; সফল হলে কিছু করে না।
  • handle((value, ex) -> …) — সফল বা ব্যর্থ, দুই ক্ষেত্রেই চলে; একটি null থাকে। ফল আর ভুল দুটো দেখে সিদ্ধান্ত নিতে।
  • orTimeout(t, unit) (Java 9) — সময়ের মধ্যে না এলে TimeoutException দিয়ে ব্যর্থ করে। completeOnTimeout(value, t, unit) (Java 9) — সময়ের মধ্যে না এলে বিকল্প মান দিয়ে সফল করে।
import java.util.concurrent.*;

class Main {
    static <T> T slowCall(int ms, T value) {                 // ধীর নেটওয়ার্ক কলের ভান
        try { Thread.sleep(ms); } catch (InterruptedException e) { throw new RuntimeException(e); }
        return value;
    }
    static int ratingService() { slowCall(50, 0); throw new IllegalStateException("রেটিং সার্ভার বন্ধ"); }

    public static void main(String[] args) {
        try (ExecutorService io = Executors.newFixedThreadPool(4)) {
            var price = CompletableFuture.supplyAsync(() -> slowCall(200, 1250), io);
            var stock = CompletableFuture.supplyAsync(() -> slowCall(100, 7), io);
            var delivery = CompletableFuture.supplyAsync(() -> slowCall(900, "কাল"), io)
                .completeOnTimeout("৩-৫ দিন (আনুমানিক)", 300, TimeUnit.MILLISECONDS);
            var rating = CompletableFuture.supplyAsync(Main::ratingService, io)
                .thenApply(r -> r + "/5")
                .exceptionally(ex -> "রেটিং নেই (" + ex.getClass().getSimpleName() + ")");

            String page = price.thenCombine(stock, (p, s) -> "দাম " + p + " টাকা, স্টকে " + s + "টি")
                .thenCombine(delivery, (ps, d) -> ps + "\nডেলিভারি: " + d)
                .thenCombine(rating, (pd, r) -> pd + "\n" + r)
                .join();
            System.out.println(page);
        }
    }
}

আউটপুট প্রতিবার একই তিন লাইন: দাম 1250 টাকা, স্টকে 7টি, ডেলিভারি: ৩-৫ দিন (আনুমানিক), রেটিং নেই (CompletionException). চারটি কল একসাথে শুরু হয়েছে (fan-out), তিনটি thenCombine এক জায়গায় জুড়েছে (fan-in), আর join() শুধু একদম শেষে। ডেলিভারি সার্ভিসের ৯০০ ms লাগত, কিন্তু ৩০০ ms-এ completeOnTimeout বিকল্প মান বসিয়ে দিয়েছে — সময়ের ব্যবধান এত বড় যে ফল কখনো বদলায় না। রেটিং সার্ভিস exception ছুড়েছে; thenApply চলেইনি, ব্যর্থতা সোজা exceptionally-তে গেছে। লক্ষ করুন, ex আসল IllegalStateException নয় — নির্ভরশীল ধাপে পৌঁছানো ভুল CompletionException-এ মোড়ানো থাকে; আসল কারণ ex.getCause(). একটি সূক্ষ্ম কথা: timeout ফল বদলায়, কিন্তু ডেলিভারির কাজটি থামায় না — সেটা ব্যাকগ্রাউন্ডে ৯০০ ms পর্যন্ত চলে, আর close() তার জন্যও অপেক্ষা করে।

টাকার লেনদেনে timeout — সাবধান

পেমেন্ট গেটওয়ের কলে তিন সেকেন্ডের সীমা দেওয়া স্বাভাবিক:

CompletableFuture<String> status = CompletableFuture
    .supplyAsync(() -> gateway.charge(orderId, amount), paymentPool)
    .orTimeout(3, TimeUnit.SECONDS)
    .exceptionally(ex -> "PENDING");        // গ্রাহককে “যাচাই চলছে” দেখান

কিন্তু ওপরের উদাহরণের শিক্ষা মনে রাখুন: timeout শুধু আপনার অপেক্ষা থামায়, গেটওয়ের কাজ নয়। টাকা হয়তো ঠিকই কেটেছে, উত্তর আসতে দেরি হয়েছে। তাই ফলব্যাক কখনো “ব্যর্থ, আবার চেষ্টা করুন” নয় — তাতে দুবার টাকা কাটতে পারে। ঠিক উপায় হলো “PENDING” অবস্থা রেখে পরে গেটওয়ের কাছে লেনদেনের অবস্থা যাচাই করা, আর প্রতিটি অনুরোধে একটি অনন্য idempotency key পাঠানো, যাতে একই অনুরোধ দুবার এলে গেটওয়ে একবারই কাটে। bKash বা কার্ড পেমেন্টে “টাকা কেটেছে কিন্তু অর্ডার হয়নি” অভিযোগের বড় একটি উৎস ঠিক এই জায়গা।

৮ · নির্দিষ্ট সময় পরপর কাজ, structured concurrency, আর অন্য ভাষার সাথে তুলনা

ScheduledExecutorService

প্রতি ৩০ সেকেন্ডে অপেক্ষমাণ পেমেন্টগুলোর অবস্থা দেখা, প্রতি রাতে একটি রিপোর্ট — এর জন্য ScheduledExecutorService:

ScheduledExecutorService timer = Executors.newScheduledThreadPool(1);
timer.schedule(() -> sendReminder(cart), 10, TimeUnit.MINUTES);          // একবার, ১০ মিনিট পরে
timer.scheduleAtFixedRate(this::checkPendingPayments, 0, 30, TimeUnit.SECONDS);
// অ্যাপ বন্ধের সময়: timer.shutdown();

scheduleAtFixedRate শুরুর সময় ধরে গোনে (০, ৩০, ৬০ সেকেন্ড…); কাজ দেরি করলে পরেরটা দেরিতে শুরু হয়, কিন্তু একই কাজ কখনো একসাথে দুবার চলে না। scheduleWithFixedDelay গোনে আগের কাজ শেষ হওয়ার পর থেকে। একটি ফাঁদ: পর্যায়ক্রমিক কাজের কোনো একবার exception ছুড়লে পরের সব বার নীরবে বন্ধ হয়ে যায় — কোনো লগ ছাড়া। তাই কাজের পুরো শরীর try-catch-এ মুড়ে ভুল লগ করুন। পুরোনো java.util.Timer একটি thread-এ চলে আর একটি exception-এ পুরোটাই থেমে যায়; নতুন কোডে এটি এড়িয়ে চলুন।

Structured concurrency — একটি প্রথম ঝলক

CompletableFuture-এর একটি সমস্যা ওপরে দেখেছেন: একটি কাজ ব্যর্থ হলে বা timeout হলে অন্য কাজগুলো চলতেই থাকে, কেউ তাদের বাতিল করে না। Structured concurrency-র ধারণা: একটি কাজ থেকে যতগুলো উপ-কাজ জন্ম নেয়, সবগুলো সেই কাজের একটি code block-এর ভেতরে শুরু ও শেষ হবে — block থেকে বেরোনোর আগে সব শেষ বা বাতিল। Java 21-এ এটি preview feature (JEP 453), চালাতে --enable-preview লাগে, আর পরের সংস্করণগুলোতে API বদলেছে। Java 21-এর রূপ:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {   // Java 21, preview
    Subtask<Integer> price = scope.fork(() -> priceService(id));
    Subtask<Integer> stock = scope.fork(() -> stockService(id));
    scope.join().throwIfFailed();      // একটি ব্যর্থ হলে বাকিগুলো বাতিল
    return render(price.get(), stock.get());
}

এটি virtual thread-এর সাথে মিলিয়ে নকশা করা, তাই বিস্তারিত পর্ব ৪২-এ। এখানে শুধু ধারণাটা মনে রাখুন: একসাথে চলা কাজের জীবনকাল code block-এর কাঠামোর সাথে বাঁধা থাকলে কোনো কাজ “হারিয়ে” পেছনে চলতে থাকে না।

C/C++/Python জানলে

C-র standard library-তে কোনো thread pool নেই; pthreads দিয়ে নিজে বানাতে হয়। C++11-এর std::async আর std::future Java-র Future-এর কাছাকাছি, তবে future.get() আসল exception-টিই আবার ছোড়ে, Java-র মতো ExecutionException-এ মুড়ে নয় — দেখুন C++-এর async, future আর atomics. Python-এর concurrent.futures.ThreadPoolExecutor প্রায় হুবহু Java-র নকশা: submit দেয় Future, result() অপেক্ষা করে, with block শেষে pool বন্ধ — তবে GIL-এর কারণে CPU-নির্ভর কাজে thread-এ লাভ সীমিত; দেখুন Python-এর thread, process আর GIL.

পরীক্ষা ও ইন্টারভিউতে
  • Runnable আর Callable-এর পার্থক্য? — run() কিছু ফেরত দেয় না আর checked exception ছুড়তে পারে না; call() ফল দেয় আর throws Exception.
  • shutdown() আর shutdownNow()? — প্রথমটি নতুন কাজ বন্ধ করে কিন্তু জমা কাজ শেষ করে; দ্বিতীয়টি চলমান কাজে interrupt পাঠায় আর শুরু না হওয়া কাজের তালিকা ফেরত দেয়।
  • Future.get() কাজের exception কীভাবে দেয়? — ExecutionException-এ মুড়ে; আসলটি getCause()-এ।
  • thenApply আর thenCompose? — ফল রূপান্তর বনাম যে function নিজেই CompletableFuture দেয় তাকে সমতলভাবে জোড়া (map বনাম flatMap).
  • newCachedThreadPool-এর ঝুঁকি? — thread-এর উপরের সীমা নেই; ধীর কাজের ঢেউয়ে হাজার হাজার thread তৈরি হয়ে মেমরি ও CPU ফুরায়।

৯ · AI-এর যুগে — ঝুলে থাকা প্রোগ্রাম আর গিলে ফেলা exception

Concurrency-র কোড AI খুব আত্মবিশ্বাসের সাথে লেখে, আর সেটা প্রায়ই ছোট টেস্টে ঠিকঠাক চলে। ভুল ধরা পড়ে ভিড়ের সময়, সার্ভারে, রাত দুটোয়। AI-এর লেখা executor কোডে বারবার যা দেখা যায়:

AI যা করেকেন সমস্যাআপনি যা করবেন
“দ্রুত করতে” newCachedThreadPool()thread-এর সীমা নেই; ধীর সার্ভিসের দিনে হাজার threadসীমিত pool, বা bounded queue-সহ ThreadPoolExecutor
shutdown বা close নেইmain শেষ হলেও প্রোগ্রাম ঝুলে থাকে; সার্ভারে thread জমতে থাকেtry-with-resources, অথবা অ্যাপ বন্ধের সময় নিশ্চিত shutdown
লুপের ভেতরে submit(...).get()প্রতিটি কাজের পর অপেক্ষা — সমান্তরালতা শূন্যআগে সব জমা, তারপর ক্রমমতো get, বা invokeAll
executor ছাড়া supplyAsync-এ ডেটাবেস/HTTP কলcommon pool আটকে যায়, parallel stream-ও থামেI/O-র জন্য আলাদা executor
submit করে Future ফেলে দেওয়া; শিকলের শেষে exceptionally(ex -> null)exception নীরবে হারায়; null পরে অন্য জায়গায় NPE ঘটায়ভুল লগ করুন, অর্থবহ বিকল্প দিন, অথবা ছড়াতে দিন
সময়সীমা ছাড়া get(), join(), await()একটি আটকে থাকা সার্ভিসে পুরো অনুরোধ চিরকাল আটকেget(t, unit), orTimeout, await(t, unit)
শেষ হওয়ার ক্রমে ফল ছাপা, বা thread-এর নাম ছাপাপ্রতিবার আলাদা আউটপুট; টেস্ট মাঝে মাঝে ফেলজমার ক্রমে ফল নেওয়া; আউটপুট নির্দিষ্ট রাখা

Executor বা CompletableFuture-এর কোড লেখাতে বা রিভিউ করাতে এই কাঠামো দিন — শর্তগুলো ওপরের প্রতিটি ভুলের দরজা বন্ধ করে:

আমি Java 21 শিখছি; ExecutorService, Future আর CompletableFuture-এর মূল ধারণা জানি। কাজ: [যেমন — একটি পণ্যের পেজের জন্য দাম, স্টক আর ডেলিভারি সার্ভিস একসাথে ডেকে ফল জোড়া দেওয়া] শর্ত: ১. newCachedThreadPool ব্যবহার করবে না; pool-এর আকার কত আর কেন, এক লাইনে লিখবে। ২. ExecutorService try-with-resources-এ রাখবে, যাতে প্রোগ্রাম শেষে বন্ধ হয়। ৩. সব কাজ আগে জমা দেবে; ফল নেবে জমার ক্রমে, বা allOf/thenCombine দিয়ে জোড়া দিয়ে শেষে একবার join করবে। ৪. ব্লক করে এমন কোনো কল executor ছাড়া supplyAsync-এ দেবে না। ৫. প্রতিটি বাইরের কলে সময়সীমা দেবে (orTimeout বা get(t, unit)). ৬. কোনো exception গিলে ফেলবে না: exceptionally/handle-এ ভুল লগ করবে আর অর্থবহ বিকল্প দেবে; null ফেরত দেবে না। ৭. thread-এর নাম বা শেষ হওয়ার ক্রম ছাপবে না; আউটপুট প্রতিবার একই হতে হবে। ৮. শেষে বলো, কোন কলগুলো একে অপরের ওপর নির্ভরশীল (thenCompose) আর কোনগুলো স্বাধীন (thenCombine).

AI-এর concurrency কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১java.util.concurrent কোথা থেকে এল? একজন অধ্যাপকের ব্যক্তিগত library কীভাবে Java-র অংশ হলো?

শুরুর সমস্যা। Java ১৯৯৬ সালে এসেছিল thread আর synchronized ভাষার ভেতরেই নিয়ে — সেই সময়ের মূলধারার ভাষায় এটা বিরল ছিল। কিন্তু হাতিয়ার ছিল খুব নিচু স্তরের: Thread, wait, notify. প্রতিটি দল নিজের thread pool, নিজের সারি, নিজের “কাজ শেষ হলে জানাও” ব্যবস্থা লিখত। এগুলো লেখা কঠিন, আর ভুল খুব সূক্ষ্ম — একটি notify-এর জায়গায় notifyAll না দিলে মাসে একবার প্রোগ্রাম আটকে যায়।

Doug Lea-র library. নিউ ইয়র্কের SUNY Oswego-র অধ্যাপক Doug Lea ১৯৯৬ সালে লেখেন Concurrent Programming in Java, আর ১৯৯০-এর দশকের শেষে প্রকাশ করেন একটি মুক্ত library, নাম util.concurrent. তাতে ছিল thread pool, বিভিন্ন ধরনের সারি, latch, barrier, semaphore — বছরের পর বছর গবেষণা আর বাস্তব ব্যবহারে পরীক্ষিত। অনেক বড় প্রজেক্ট এটি ব্যবহার শুরু করে।

JSR 166. Java-তে নতুন অংশ যোগ হয় Java Community Process-এর মাধ্যমে, একটি JSR (Java Specification Request) দিয়ে। Doug Lea-র নেতৃত্বে JSR 166-এর বিশেষজ্ঞ দল তাঁর library-কে নতুন করে নকশা করে, আর সেপ্টেম্বর ২০০৪-এ Java 5-এর সাথে আসে java.util.concurrent: Executor, ExecutorService, Future, Callable, BlockingQueue, ConcurrentHashMap, CountDownLatch, Semaphore, CyclicBarrier, আর atomic variable. একই সময়ে JSR 133 Java Memory Model নতুন করে লেখে, যাতে এসব হাতিয়ার কোন নিশ্চয়তা দেয় তা নির্দিষ্টভাবে বলা যায়।

পরের ধাপ। একই ধারায় Java 7-এ Fork/Join, Java 8-এ CompletableFuture আর parallel stream, Java 9-এ Flow — বেশিরভাগই Doug Lea আর তাঁর সহকর্মীদের হাতে। এই library অন্য ভাষাকেও প্রভাবিত করেছে; Python-এর concurrent.futures (২০১১) নিজের নকশার প্রেরণা হিসেবে Java-র এই প্যাকেজের কথা বলে।

মূল উপলব্ধি: ভালো concurrency টুল একদিনে তৈরি হয় না; একজন গবেষকের বছরের পর বছর পরীক্ষিত library মানসম্মত হওয়ার পর লাখো প্রোগ্রামারকে হাতে লেখা wait/notify-এর ঝুঁকি থেকে মুক্তি দিয়েছে — তাই নিজে বানানোর আগে java.util.concurrent-এ খুঁজুন।

প্র ০২“Future” আর “promise” — যে ফল এখনো আসেনি তার প্রতিনিধি। এই ধারণা কবে, কোথায় জন্মেছিল?

১৯৭০-এর দশকের গবেষণা। ধারণাটি এসেছে প্রোগ্রামিং ভাষার গবেষণা থেকে। ১৯৭৬ সালে Daniel Friedman আর David Wise একটি গবেষণাপত্রে “promise” শব্দটি ব্যবহার করেন — এমন একটি মান, যার হিসাব পরে হবে, কিন্তু প্রোগ্রাম এখনই সেটাকে হাতে-হাতে চালাতে পারে। ১৯৭৭ সালে MIT-এর Henry Baker আর Carl Hewitt “future” শব্দটি দেন, Hewitt-এর actor model-এর প্রেক্ষাপটে: একটি হিসাব আলাদাভাবে শুরু করে দাও, আর তার ফলের জায়গায় একটি future রাখো; কেউ সত্যিই মানটি চাইলে তবেই অপেক্ষা।

বাস্তব ভাষায়। ১৯৮০-এর দশকে Robert Halstead-এর Multilisp-এ (future expr) সমান্তরাল হিসাবের মূল উপায় হয়। ১৯৮৮ সালে Barbara Liskov আর Liuba Shrira দূরের সার্ভারে কলের জন্য promise নকশা করেন, যাতে উত্তরের অপেক্ষা না করেই পরের কল পাঠানো যায়। অর্থাৎ নেটওয়ার্কের দেরি লুকানো — আজকের microservice-এর সমস্যাই।

Java-র দুই ধাপ। Java 5-এর Future (২০০৪) ধারণাটির সবচেয়ে সরল রূপ: শুধু অপেক্ষা (get), বাতিল আর অবস্থা দেখা। ফল এলে “নিজে থেকে কিছু করো” বলার উপায় ছিল না। এই ফাঁক পূরণ করতে Google-এর Guava library আনে ListenableFuture, আর Java 8 (২০১৪) আনে CompletableFuture ও CompletionStage interface. নাম “Completable” কারণ যে কেউ বাইরে থেকে complete(value) ডেকে এটিকে শেষ করতে পারে — অনেক ভাষায় যাকে বলে promise.

অন্য জগতে। JavaScript-এ Promise মানসম্মত হয় ES2015-এ, আর তার ওপর async/await আসে ES2017-এ; C#-এ async/await আসে ২০১২ সালে। Java async/await যোগ করেনি; পরে বেছে নিয়েছে অন্য পথ — virtual thread, যেখানে সাধারণ blocking কোডই সস্তা হয়ে যায় (পর্ব ৪২)।

মূল উপলব্ধি: Future/promise আসলে একটি পুরোনো, গভীর ধারণা — “মানটি এখনো নেই, কিন্তু তার প্রতিনিধি আছে” — আর প্রতিটি ভাষা সেটাকে নিজের মতো করে নিয়েছে: blocking get, callback শিকল, async/await, বা সস্তা thread.

প্র ০৩“Callback hell” কী, আর CompletableFuture-এর শিকল কীভাবে তা থেকে কিছুটা মুক্তি দেয় — কিন্তু পুরোটা নয়?

সমস্যাটা। অপেক্ষা না করে কাজ করার পুরোনো উপায় হলো callback: “এই কাজটা করো, শেষ হলে এই function ডেকো।” একটি কাজ হলে ঠিক আছে। কিন্তু ব্যবহারকারী খোঁজো → তার অর্ডার আনো → প্রতিটি অর্ডারের ডেলিভারি অবস্থা আনো → সব জুড়ে পেজ বানাও — callback দিয়ে লিখলে প্রতিটি ধাপ আগেরটির ভেতরে ঢোকে, কোড ডান দিকে সরতে সরতে পিরামিডের মতো হয়। প্রতিটি স্তরে আলাদা error callback, আর একটি ভুলে যাওয়া মানে ভুল নীরবে হারানো। Node.js-এর প্রথম দিকের JavaScript জগৎ এই অবস্থার নাম দেয় callback hell.

CompletableFuture-এর সমাধান। শিকল ভেতরের দিকে নয়, নিচের দিকে বাড়ে: findUser(id).thenCompose(this::orders).thenApply(this::render). প্রতিটি ধাপ একটি মান (CompletableFuture) ফেরত দেয়, তাই ধাপগুলো জোড়া যায়, ফেরত দেওয়া যায়, তালিকায় রাখা যায়। ভুল নিজে থেকেই শিকল বেয়ে নিচে নামে; একটি exceptionally পুরো শিকলের ভুল ধরতে পারে — ঠিক try-catch-এর মতো। thenCombine আর allOf সমান্তরাল কাজ জোড়ার পরিষ্কার উপায় দেয়, যা callback-এ খুব কঠিন।

যা থেকে যায়। তবুও এটি সাধারণ Java নয়। লুপ লেখা যায় না (একটির পর একটি ১০টি কল, শর্তসহ — জটিল recursion লাগে), সাধারণ try-catch চলে না, checked exception lambda-য় ঢোকে না। Stack trace-এ আপনার নিজের method প্রায় থাকে না, থাকে CompletableFuture-এর ভেতরের যন্ত্র — debug কঠিন। কোন ধাপ কোন thread-এ চলবে (thenApply বনাম thenApplyAsync) তা নিয়ে সূক্ষ্ম ভুল হয়। আর একটি ভুলে-যাওয়া exceptionally এখনো ভুল গিলে ফেলতে পারে।

কখন কোনটা। কয়েকটি স্বাধীন কল একসাথে চালিয়ে জোড়া দেওয়া — CompletableFuture চমৎকার। দীর্ঘ, শাখাযুক্ত ব্যবসায়িক যুক্তি — সাধারণ ধারাবাহিক কোড অনেক পরিষ্কার, আর Java 21-এর virtual thread সেই কোডকেই সস্তায় হাজারে হাজারে চালাতে দেয়।

মূল উপলব্ধি: CompletableFuture callback-এর পিরামিডকে একটি পড়ার মতো শিকলে বদলেছে, কিন্তু শিকল সাধারণ কোডের সব সুবিধা (লুপ, try-catch, পরিষ্কার stack trace) দেয় না — তাই একে জোড়া দেওয়ার হাতিয়ার হিসেবে ব্যবহার করুন, পুরো প্রোগ্রাম লেখার ভাষা হিসেবে নয়।

প্র ০৪Pool-এর আকার — অনুমান না হিসাব? Little-এর সূত্র আর Goetz-এর সূত্র কী বলে, আর কোথায় এদের সীমা?

Little-এর সূত্র। পর্ব ৩২-এ দেখেছেন L = λW: একটি স্থিতিশীল সিস্টেমে গড়ে ভেতরে কতজন (L) = আসার হার (λ) × প্রত্যেকের থাকার সময় (W). একটি API সার্ভারে সেকেন্ডে ৫০০টি অনুরোধ আসে, প্রতিটি ২০০ ms নেয়: L = ৫০০ × ০.২ = ১০০. প্রতিটি অনুরোধ পুরো সময় একটি thread আটকে রাখলে, গড়ে ১০০টি thread একসাথে ব্যস্ত। Pool ৫০ হলে বাকিরা সারিতে অপেক্ষা করবে, অপেক্ষা W বাড়াবে, আর W বাড়লে L আরও বাড়বে — সারি লম্বা হতেই থাকবে যতক্ষণ না ভিড় কমে। সূত্রটি কোনো অনুমানের ওপর নির্ভর করে না বলেই এত শক্তিশালী।

Goetz-এর সূত্র। Brian Goetz আর সহলেখকদের Java Concurrency in Practice (২০০৬) একটি core-কেন্দ্রিক হিসাব দেয়: Nthreads = Ncpu × Ucpu × (১ + W/C), যেখানে U কাঙ্ক্ষিত CPU ব্যবহার (০ থেকে ১), W অপেক্ষার সময়, C হিসাবের সময়। পুরো CPU-নির্ভর কাজে W ≈ ০, তাই thread ≈ core (বইটি একটি বাড়তি thread রাখার পরামর্শ দেয়, যাতে কোনো thread page fault-এ থামলে core খালি না থাকে)। ৮ core, W/C = ৯ হলে প্রায় ৮০।

দুটি একসাথে। Little বলে চাহিদা থেকে কতগুলো একসাথে লাগবে; Goetz বলে CPU পুরো ব্যস্ত রাখতে কতগুলো thread যথেষ্ট। কম সংখ্যাটি প্রায়ই আসল সীমা। আর thread একমাত্র সম্পদ নয়: ডেটাবেস connection pool যদি ২০টি হয়, ২০০ thread দিলেও ডেটাবেসে একসাথে ২০টির বেশি কাজ যাবে না — বাকিরা connection-এর অপেক্ষায় বসে থাকবে। সবচেয়ে সরু জায়গাটিই পুরো সিস্টেমের গতি ঠিক করে।

সীমা। W আর C স্থির নয় — ডেটাবেস ধীর হলে W হঠাৎ দশগুণ, তখন pool পুরো ভরে যায়। তাই সীমিত সারি, timeout আর rejection policy দরকার, যাতে একটি ধীর নির্ভরতা পুরো সার্ভার না বসায়। আর platform thread দামি বলেই এত হিসাব; Java 21-এর virtual thread-এ প্রতিটি অনুরোধে একটি thread দেওয়া সস্তা, তখন হিসাব সরে যায় সত্যিকারের সীমিত সম্পদে — connection, API-র rate limit — যেখানে Semaphore কাজে আসে।

মূল উপলব্ধি: Pool-এর আকার কোনো জাদুর সংখ্যা নয়; Little-এর সূত্র চাহিদা মাপে, Goetz-এর সূত্র CPU মাপে, আর আসল উত্তর আসে সবচেয়ে সরু সম্পদ চিনে load test দিয়ে মেপে।

প্র ০৫RxJava আর Project Reactor-এর reactive programming বনাম Java 21-এর virtual thread — বিতর্কটা কী নিয়ে?

Reactive-এর জন্ম। ২০১০-এর দশকে বড় ওয়েব কোম্পানিগুলো দেখল, প্রতিটি অনুরোধে একটি platform thread দিলে কয়েক হাজার একসাথে অনুরোধেই সার্ভারের সীমা আসে — thread-গুলো বেশিরভাগ সময় শুধু নেটওয়ার্কের অপেক্ষায় বসে থাকে। সমাধান হিসেবে জনপ্রিয় হয় reactive programming: অল্প কয়েকটি thread, আর ডেটাকে ঘটনার প্রবাহ (stream of events) হিসেবে দেখা, যেখানে প্রতিটি ধাপ ডেটা এলে সাড়া দেয়, কখনো অপেক্ষা করে না। Microsoft-এর Reactive Extensions (Rx)-এর ধারণা থেকে Netflix তৈরি করে RxJava; Pivotal (Spring-এর পেছনের কোম্পানি) তৈরি করে Project Reactor, যার ওপর Spring WebFlux (২০১৭)। Reactive Streams নির্দিষ্টকরণ backpressure-এর নিয়ম ঠিক করে — ধীর consumer দ্রুত producer-কে “ধীরে দাও” বলতে পারে — আর Java 9-এ সেটা আসে java.util.concurrent.Flow হিসেবে।

দাম। Reactive কোড দক্ষ, কিন্তু লেখা, পড়া আর debug করা কঠিন। flatMap, zip, onErrorResume-এর শিকলে সাধারণ লুপ আর try-catch নেই, stack trace প্রায় অর্থহীন, আর একটি blocking কল ভুল করে ঢুকে পড়লে পুরো সিস্টেম ধীর হয়। পুরো কোডবেস — ডেটাবেস driver পর্যন্ত — reactive হতে হয়।

Virtual thread-এর পাল্টা যুক্তি। Project Loom (২০১৭ সালে ঘোষিত, Ron Pressler-এর নেতৃত্বে) উল্টো প্রশ্ন করে: সমস্যা যদি thread-এর দাম হয়, thread-কেই সস্তা করা যাক। Java 21-এ (JEP 444) virtual thread স্থায়ী হয়: লাখো thread সম্ভব, আর blocking কল করলে JVM নিজেই নিচের OS thread ছেড়ে দেয়। ফলে সাধারণ, ধারাবাহিক, পড়ার মতো কোডই reactive-এর মতো মাপে বাড়ে। অনেকে তাই মনে করেন, শুধু thread বাঁচানোর জন্য reactive লেখার যুগ শেষ হয়ে আসছে।

Reactive তবুও কোথায় টেকে। যেখানে সমস্যাটা সত্যিই প্রবাহের — লাইভ ডেটা ফিড, backpressure, সময়ভিত্তিক জানালা (প্রতি ৫ সেকেন্ডের গড়), একাধিক ঘটনার উৎস মেশানো — সেখানে reactive operator-গুলো এখনো সবচেয়ে প্রকাশক্ষম ভাষা। আর বিদ্যমান বড় reactive কোডবেস রাতারাতি বদলায় না।

মূল উপলব্ধি: Reactive programming আর virtual thread একই সমস্যার — অপেক্ষারত thread-এর অপচয় — দুটি ভিন্ন উত্তর; একটি কোডের ধরন বদলায়, অন্যটি runtime বদলায়। পর্ব ৪২-এ দেখবেন কেন Java ২০২৩ সালে দ্বিতীয় পথটিকে মূলধারা বানাল।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — কোন কাজ কখন, কোথায় অপেক্ষা করে, সেটা ভাবাই আসল কাজ।

  1. সময় অনুমান করুন: ৫টি thread-এর একটি pool, ৫টি কাজ, প্রতিটি ১ সেকেন্ড ঘুমায়। (ক) for (var t : tasks) results.add(pool.submit(t).get()); — মোট কত সময়? (খ) আগে সব submit, তারপর আলাদা লুপে সব get — কত? (গ) pool-এ ২টি thread হলে (খ)-তে কত?

    (ক) প্রায় ৫ সেকেন্ড। প্রতিটি get() পরের কাজ জমা দেওয়ার আগেই অপেক্ষা করে, তাই কাজগুলো একটির পর একটি চলে; বাকি চারটি thread অলস বসে থাকে। (খ) প্রায় ১ সেকেন্ড — পাঁচটি কাজ পাঁচটি worker-এ একসাথে চলে, প্রথম get()-এ অপেক্ষার মধ্যেই বাকিগুলোও শেষ হয়। (গ) প্রায় ৩ সেকেন্ড — প্রথম সেকেন্ডে ২টি, দ্বিতীয়তে ২টি, তৃতীয়তে শেষ ১টি। সারি তখন অপেক্ষার জায়গা।

  2. Bug খুঁজুন — প্রোগ্রাম শেষ হয় না: একজন সহকর্মী লিখেছেন ExecutorService pool = Executors.newFixedThreadPool(4);, তারপর কয়েকটি কাজ submit, ফল ছাপা — সব ঠিক ছাপে, কিন্তু টার্মিনাল আর ফেরে না, চাপতে হয় Ctrl+C. কেন, আর দুটি সমাধান কী?

    Pool-এর worker-রা non-daemon thread, আর কাজ শেষে তারা মারা যায় না, সারিতে নতুন কাজের অপেক্ষায় বসে থাকে। JVM সব non-daemon thread শেষ না হলে বন্ধ হয় না, তাই main শেষ হলেও প্রোগ্রাম চলতে থাকে। সমাধান ১: Java 19 বা পরে — try (ExecutorService pool = Executors.newFixedThreadPool(4)) { … }, block শেষে close() বন্ধ করে আর অপেক্ষা করে। সমাধান ২ (পুরোনো Java-তেও): কাজ জমা শেষে pool.shutdown(), তারপর দরকার হলে pool.awaitTermination(...). (newCachedThreadPool-এ প্রোগ্রাম শেষমেশ ৬০ সেকেন্ড পরে বেরোত, কারণ খালি thread সেখানে মারা যায় — এতে ভুলটা আরও রহস্যময় লাগে।)

  3. Pool-এর আকার হিসাব করুন: একটি রিপোর্ট সার্ভিস ৪ core-এর মেশিনে চলে। প্রতিটি অনুরোধে ৪০ ms ডেটাবেসের অপেক্ষা আর ১০ ms হিসাব। সেকেন্ডে ২০০টি অনুরোধ আসে। Goetz আর Little দিয়ে আলাদাভাবে হিসাব করুন, আর ডেটাবেসের connection pool যদি ১৫টি হয়, কী বলবেন?

    Goetz: ৪ × (১ + ৪০/১০) = ৪ × ৫ = ২০টি thread হলে CPU মোটামুটি পুরো ব্যস্ত। Little: প্রতিটি অনুরোধ থাকে ৫০ ms = ০.০৫ সেকেন্ড, তাই L = ২০০ × ০.০৫ = গড়ে ১০টি অনুরোধ একসাথে — ১০-এর বেশি thread লাগবে, ভিড়ের ওঠানামার জন্য কিছু বাড়তি রেখে ১৫–২০ যুক্তিসঙ্গত। কিন্তু connection pool ১৫ হলে একসাথে ১৫টির বেশি অনুরোধ ডেটাবেসে যেতেই পারবে না; ৫০টি thread দিলেও বাকিরা connection-এর অপেক্ষায় বসে থাকবে। তাই thread pool প্রায় ১৫–২০, আর আসল সংখ্যা load test করে ঠিক করুন।

  4. নকশা করুন — চেকআউট পেজ: অর্ডার দেওয়ার সময় চারটি কাজ: (ক) কার্ট থেকে মোট দাম, (খ) ঠিকানা থেকে ডেলিভারি চার্জ, (গ) মোট দাম আর ডেলিভারি চার্জ জেনে কুপন যাচাই, (ঘ) চূড়ান্ত দামে পেমেন্ট। কোনগুলো একসাথে চলতে পারে, কোথায় thenCombine, কোথায় thenCompose, আর কোথায় timeout-এর ফলব্যাক বিপজ্জনক?

    (ক) আর (খ) স্বাধীন — দুটি supplyAsync একসাথে, তারপর cartTotal.thenCombine(deliveryFee, Integer::sum). (গ)-এর জন্য দুটির ফল লাগে, আর কুপন সার্ভিস নিজেই async কল — তাই .thenCompose(total -> couponService.apply(code, total)). (ঘ) চূড়ান্ত দামের ওপর নির্ভরশীল — আবার thenCompose(finalPrice -> payment.charge(finalPrice)). Timeout: কুপন সার্ভিস ধীর হলে completeOnTimeout(ছাড় ছাড়া দাম, …) নিরাপদ, গ্রাহককে জানিয়ে দিলেই হলো। কিন্তু পেমেন্টে timeout-এর ফলব্যাক “ব্যর্থ” বা “আবার চেষ্টা” হলে টাকা দুবার কাটতে পারে, কারণ timeout আসল কাজ থামায় না; সেখানে “PENDING” রেখে পরে যাচাই, আর idempotency key.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমJava-তে একটি ExecutorService দিয়ে কয়েকটি কাজ চালানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🏭 Executors.newFixedThreadPool(3) দিয়ে pool তৈরি
  2. 🧾 সব কাজ submit করে প্রতিটির জন্য একটি Future নেওয়া
  3. ⚙️ worker-রা সারি থেকে কাজ তুলে চালায়, খালি হলে পরের কাজ নেয়
  4. 🤝 জমার ক্রমে প্রতিটি Future.get() দিয়ে ফল সংগ্রহ
  5. 🔒 close() বা shutdown() — নতুন কাজ বন্ধ, worker-রা বিদায় নেয়, প্রোগ্রাম শেষ হতে পারে

Java-তে একটি Callable ExecutorService-এ submit করা হলো, আর কাজটি চলার সময় IllegalStateException ছুড়ল। পরে future.get() ডাকলে কী ঘটে?

  • সরাসরি IllegalStateException ছোড়ে
  • ExecutionException ছোড়ে, যার cause হলো IllegalStateException
  • null ফেরত দেয়
  • কাজ ব্যর্থ হওয়ার মুহূর্তেই মূল thread থেমে যায়

Java-তে findUser(id) দেয় CompletableFuture<User>, আর lastOrder(user) দেয় CompletableFuture<Order>. CompletableFuture<Order> পেতে findUser(id)-এর পরে কোন method?

  • thenCompose(u -> lastOrder(u))
  • thenApply(u -> lastOrder(u))
  • thenCombine(lastOrder(u), …)
  • thenAccept(u -> lastOrder(u))

Java-তে একটি ExecutorService-এর shutdown() ডাকলে কী ঘটে?

  • চলমান সব কাজে সাথে সাথে interrupt পাঠায়
  • সব কাজ শেষ হওয়া পর্যন্ত ডাকা thread আটকে থাকে
  • সারিতে অপেক্ষমাণ কাজগুলো বাদ দেয়
  • নতুন কাজ নেয় না, কিন্তু আগে জমা দেওয়া কাজ শেষ হতে দেয়; নিজে অপেক্ষা করে না

৮ core-এর একটি মেশিনে শুধু CPU-নির্ভর হিসাব (ছবি কম্প্রেশন) চালানোর জন্য Java thread pool-এর আকার মোটামুটি কত হওয়া উচিত?

  • ১
  • প্রায় ৮ (core সংখ্যার সমান)
  • প্রায় ২০০
  • সীমা ছাড়া, যত কাজ তত thread

Java-তে new Semaphore(3) দিয়ে সুরক্ষিত একটি ডাউনলোড অংশে (acquire → download → finally release) ১০টি thread একসাথে ঢুকতে চায়। কী ঘটে?

  • ১০টিই একসাথে ডাউনলোড করে
  • ৩টি ঢোকে, বাকি ৭টি exception পায়
  • একসাথে সর্বোচ্চ ৩টি চলে; বাকিরা অপেক্ষা করে আর permit ফেরত এলে একে একে ঢোকে
  • মোট ৩টি ডাউনলোডের পর বাকিরা চিরকাল আটকে থাকে

Java-তে CompletableFuture-এর join() আর get()-এর মূল পার্থক্য কী?

  • join() unchecked CompletionException ছোড়ে, get() checked ExecutionException ছোড়ে
  • join() অপেক্ষা করে না, get() করে
  • join() ভুল হলে null দেয়, get() exception ছোড়ে
  • join() কাজটিকে নতুন thread-এ সরিয়ে দেয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Pool = কয়েকজন স্থায়ী worker + সারি: প্রতিটি কাজে নতুন thread নয়; newFixedThreadPool বা সীমাসহ ThreadPoolExecutor; newCachedThreadPool-এর সীমা নেই — সাবধান।
  2. Future রসিদ: আগে সব submit, তারপর জমার ক্রমে get (বা invokeAll); কাজের exception আসে ExecutionException-এ মুড়ে, আসলটি getCause()-এ; অপেক্ষায় সময়সীমা দিন।
  3. বন্ধ করুন: try-with-resources (Java 19 থেকে) বা shutdown + awaitTermination; নইলে প্রোগ্রাম ঝুলে থাকে। CPU-নির্ভর কাজে pool ≈ core; I/O-নির্ভরে বেশি — Little আর Goetz দিয়ে শুরু, load test দিয়ে শেষ।
  4. সমন্বয়: CountDownLatch — সবাই শেষ হলে এগোও; Semaphore — একসাথে সর্বোচ্চ n; CyclicBarrier — বারবারের মিলনস্থল; BlockingQueue — producer-consumer. release/countDown সবসময় finally-তে।
  5. CompletableFuture: supplyAsync(task, নিজের executor) → thenApply; নির্ভরশীল thenCompose, স্বাধীন thenCombine/allOf; exceptionally/handle; orTimeout/completeOnTimeout অপেক্ষা থামায়, আসল কাজ নয়; শেষে একবার join.

পরের পর্বে: virtual thread. এই পর্বে বারবার দেখেছেন, platform thread দামি বলেই pool-এর আকার নিয়ে এত হিসাব, আর অপেক্ষা এড়াতে CompletableFuture-এর শিকল। Java 21-এর virtual thread প্রশ্নটাই উল্টে দেয়: লাখো thread, সাধারণ blocking কোড, আর newVirtualThreadPerTaskExecutor() — কীভাবে কাজ করে, আর কোথায় এর ফাঁদ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান