জাভায় অ্যাসিঙ্ক্রোনাস প্রোগ্রামিং আধুনিক অ্যাপ্লিকেশনের জন্য অপরিহার্য একটি দক্ষতা। যখন আপনি একাধিক কাজকে সমান্তরালে চালিয়ে পারফরম্যান্স বাড়াতে চান, তখন CompletableFuture আপনার সবচেয়ে শক্তিশালী হাতিয়ার। সাধারণ Future ইন্টারফেস ছিল সীমাবদ্ধ—এটি ব্লকিং, কম্পোজ করা যেত না এবং ম্যানুয়ালি সম্পন্ন করা কঠিন ছিল। এই টিউটোরিয়ালে আমরা CompletableFuture-এর মাধ্যমে জাভায় আধুনিক অ্যাসিঙ্ক্রোনাস কোড কীভাবে লিখতে হয়, একটির পর একটি কাজ চেইন করতে হয়, এক্সেপশন হ্যান্ডেল করতে হয় এবং একাধিক ফিউচারকে একত্র করতে হয়, তা শিখব। শেষে একটি সম্পূর্ণ রানযোগ্য উদাহরণ পাবেন যা বাস্তব জগতের সমস্যা সমাধান করে।

ভূমিকা

java.util.concurrent.Future ইন্টারফেসটি জাভা ৫ থেকে অ্যাসিঙ্ক্রোনাস কাজের ফলাফল ধারণ করার জন্য ব্যবহৃত হয়ে আসছে। কিন্তু এর কিছু বড় সীমাবদ্ধতা আছে: আপনি একটি Future-কে অন্যটির সাথে কম্পোজ করতে পারেন না, ফলাফল পাওয়া গেলে স্বয়ংক্রিয়ভাবে কোনো কাজ চালানোর ব্যবস্থা নেই, এবং get() মেথড কল করলে থ্রেড ব্লক হয়ে যায়। CompletableFuture জাভা ৮-তে এই সব সমস্যার সমাধান হিসেবে এসেছে, যা Future এবং CompletionStage ইন্টারফেস দুটোই ইমপ্লিমেন্ট করে। এটি একটি প্রমিস/অ্যাসিঙ্ক্রোনাস কম্পিউটেশন মডেল প্রদান করে, যেখানে আপনি ফাংশনাল স্টাইলে অ্যাসিঙ্ক্রোনাস টাস্ক তৈরি, চেইন, কম্বাইন এবং এক্সেপশন হ্যান্ডেল করতে পারেন।

মনে করুন, একটি ই-কমার্স অ্যাপে আপনাকে একই সাথে ইউজারের তথ্য, তার অর্ডার হিস্ট্রি এবং পেমেন্ট স্ট্যাটাস আনতে হবে। এই তিনটি কাজ সম্পূর্ণ স্বাধীন—এগুলোকে প্যারালালে চালিয়ে তারপর ফলাফল একত্র করলে রেসপন্স টাইম অনেক কমে যায়। CompletableFuture এমন কো-অর্ডিনেশন অত্যন্ত সহজ করে তোলে। এর মূল শক্তি হচ্ছে নন-ব্লকিং কম্পোজিশন: আপনি ল্যাম্বডা এক্সপ্রেশন দিয়ে একটির পর একটি ধাপ সাজাতে পারেন, কোনো থ্রেড আটকে না রেখে।

পূর্বশর্ত

এই টিউটোরিয়ালটি অনুসরণ করতে আপনার যা যা জানা বা থাকা দরকার:

  • জাভা ৮ বা তার পরবর্তী ভার্শন ইনস্টল থাকা এবং বেসিক জাভা সিনট্যাক্সের উপর ধারণা।
  • ল্যাম্বডা এক্সপ্রেশন ও ফাংশনাল ইন্টারফেস বিষয়ে প্রাথমিক জ্ঞান।
  • মাল্টি-থ্রেডিং ও কনকারেন্সির বেসিক ধারণা (থ্রেড, এক্সিকিউটর সার্ভিস ইত্যাদি)।
  • যেকোনো আইডিই (IntelliJ, Eclipse, VS Code) এবং জাভা ক্লাস রান করতে পারা।
  • প্রোজেক্ট বিল্ড টুল হিসেবে Maven বা Gradle ব্যবহার করতে জানা (উদাহরণে কোনো নির্ভরশীলতা লাগবে না, তবে রান করার জন্য)।

আমরা পুরো টিউটোরিয়ালজুড়ে জাভার স্ট্যান্ডার্ড ক্লাস ব্যবহার করব, কোনো এক্সটার্নাল লাইব্রেরির প্রয়োজন নেই।

ধাপে ধাপে বাস্তবায়ন

১. প্রথম CompletableFuture তৈরি করা

CompletableFuture তৈরি করার প্রধান দুটি স্ট্যাটিক ফ্যাক্টরি মেথড হলো supplyAsync এবং runAsyncsupplyAsync একটি সাপ্লায়ার থেকে ফলাফল ফেরত দেয়, আর runAsync শুধু একটি রানেবল চালায়, কোনো ফলাফল দেয় না।

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class BasicCreation {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // সরবরাহকারী দিয়ে CompletableFuture
        CompletableFuture<String> futureWithResult = CompletableFuture.supplyAsync(() -> {
            // সিমুলেটেড লম্বা কাজ
            sleep(1000);
            return "ফলাফল প্রস্তুত";
        });

        // শুধু রান করার জন্য, কোনো রিটার্ন টাইপ নেই
        CompletableFuture<Void> futureVoid = CompletableFuture.runAsync(() -> {
            sleep(500);
            System.out.println("পটভূমির কাজ শেষ");
        });

        System.out.println(futureWithResult.get()); // ব্লকিং কল
        futureVoid.get(); // নিশ্চিত হওয়া যে রান শেষ
    }

    private static void sleep(int millis) {
        try { Thread.sleep(millis); } catch (InterruptedException e) { }
    }
}

প্রথম ফিউচারটি একটি থ্রেডে supplyAsync-এর ভেতরের ল্যাম্বডাটি চালায় এবং স্ট্রিং ফলাফল প্রদান করে। দ্বিতীয়টি শুধু কনসোলে প্রিন্ট করে। get() কল করলে মেইন থ্রেড অপেক্ষা করে, তবে এটাই শেষ লক্ষ্য নয়—আমরা চেইনিং শিখে ব্লকিং এড়াব।

২. ট্রান্সফরমেশন ও চেইনিং

CompletableFuture-এর আসল শক্তি এর নন-ব্লকিং চেইনিং মেথডগুলোতে: thenApply, thenAccept, thenRun। এরা আগের স্টেজ শেষ হলে স্বয়ংক্রিয়ভাবে পরবর্তী কাজ চালায়।

CompletableFuture.supplyAsync(() -> {
    sleep(800);
    return "হ্যালো";
}).thenApply(result -> {
    return result + " ওয়ার্ল্ড";          // ফলাফল ট্রান্সফর্ম
}).thenAccept(transformed -> {
    System.out.println("চূড়ান্ত আউটপুট: " + transformed); // কনসিউম
}).thenRun(() -> {
    System.out.println("সব কাজ শেষ, রিসোর্স ক্লিনআপ করা হলো");
});

thenApply আগের ফলাফল নিয়ে নতুন কিছু তৈরি করে পরবর্তী স্টেজে পাঠায়, thenAccept ফলাফল গ্রহণ করে কিন্তু কোনোকিছু রিটার্ন করে না, thenRun কোনো ইনপুট নেয় না এবং কিছু রিটার্নও করে না। এই চেইনটি সম্পূর্ণ অ্যাসিঙ্ক্রোনাস; প্রতিটি ধাপ আগেরটি শেষ হওয়ার পরই চলে, কিন্তু থ্রেড ব্লক হয় না। আপনি যদি get() কল না করেন, মেইন থ্রেড শেষ হয়ে গেলে ডেমন থ্রেডগুলো বন্ধ হয়ে যেতে পারে, তাই প্রদর্শনের জন্য প্রায়ই join() ব্যবহার করা হয়।

৩. ফিউচার কম্পোজিশন: thenCompose ও thenCombine

দুটি অ্যাসিঙ্ক্রোনাস কাজকে পরস্পরের উপর নির্ভর করে চেইন করতে চাইলে thenCompose ব্যবহার করতে হয়। এটি ফ্ল্যাটম্যাপের মতো কাজ করে—একটি ফিউচার থেকে আরেকটি ফিউচার তৈরি করে।

CompletableFuture<String> fetchUserId = CompletableFuture.supplyAsync(() -> "user-101");

CompletableFuture<String> userDetails = fetchUserId.thenCompose(userId -> {
    return CompletableFuture.supplyAsync(() -> {
        sleep(500);
        return userId + " এর বিস্তারিত তথ্য";
    });
});

System.out.println(userDetails.join()); // user-101 এর বিস্তারিত তথ্য

অন্যদিকে, দুটি স্বাধীন ফিউচার সমান্তরালে চালিয়ে একত্র করতে thenCombine ব্যবহার হয়।

CompletableFuture<String> priceFuture = CompletableFuture.supplyAsync(() -> {
    sleep(300);
    return "মূল্য: ৳৫০০";
});
CompletableFuture<String> discountFuture = CompletableFuture.supplyAsync(() -> {
    sleep(400);
    return "ছাড়: ১০%";
});

CompletableFuture<String> finalOutput = priceFuture.thenCombine(discountFuture, (price, discount) -> {
    return price + " | " + discount;
});
System.out.println(finalOutput.join());

এই উদাহরণে priceFuture এবং discountFuture একই সময়ে চলতে থাকে এবং দুটো শেষ হলেই কেবল কম্বাইনার চলে। এতে মোট সময় দুটির মধ্যে সর্বোচ্চ সময়টির সমান হয়, যোগফল নয়।

৪. এক্সেপশন হ্যান্ডলিং

অ্যাসিঙ্ক্রোনাস জগতে এক্সেপশন হ্যান্ডলিং না করলে পুরো পাইপলাইন নিঃশব্দে ব্যর্থ হয়ে যেতে পারে। CompletableFuture তিনটি দারুণ মেথড দেয়: exceptionally, handle, এবং whenComplete

CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("ডাটাবেস ত্রুটি");
}).exceptionally(ex -> {
    System.err.println("এক্সেপশন ধরা পড়েছে: " + ex.getMessage());
    return "ফলব্যাক মান";
}).thenAccept(result -> {
    System.out.println("রেজাল্ট: " + result);
});

exceptionally শুধুমাত্র ত্রুটির ক্ষেত্রে ডাকা হয় এবং একটি রিকভারি ভ্যালু প্রদান করে, যাতে চেইন চলতে পারে। handle মেথডটি সফলতা ও ব্যর্থতা উভয় অবস্থায় ডাকা হয়; এটি (result, exception) দুটো প্যারামিটার পায়। whenComplete একইভাবে দুটো প্যারামিটার পায় কিন্তু চেইনের ফলাফল পরিবর্তন করে না।

CompletableFuture<Integer> handled = CompletableFuture.supplyAsync(() -> 10 / 0)
    .handle((res, ex) -> {
        if (ex != null) {
            System.out.println("হ্যান্ডলার: ত্রুটি -> " + ex.getMessage());
            return -1;
        }
        return res;
    });
System.out.println(handled.join()); // -1

৫. একাধিক ফিউচার পরিচালনা: allOf, anyOf

একাধিক স্বাধীন ফিউচার সম্পন্ন হওয়ার অপেক্ষা করতে allOf ব্যবহার হয়, আর যেকোনো একটি শেষ হলেই ফলাফল পেতে anyOf ব্যবহার করা হয়।

CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> { sleep(200); return "কাজ ১"; });
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> { sleep(100); return "কাজ ২"; });
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> { sleep(300); return "কাজ ৩"; });

CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.join(); // সব শেষ হওয়ার অপেক্ষা
System.out.println(f1.join() + f2.join() + f3.join()); // এখন নিরাপদে get()/join()

CompletableFuture<Object> any = CompletableFuture.anyOf(f1, f2, f3);
System.out.println("প্রথম শেষ হওয়া: " + any.join()); // সম্ভবত কাজ ২

allOf একটি Void ফিউচার ফেরত দেয়; সম্পূর্ণ শেষ হলে আপনি পৃথক ফিউচারগুলো থেকে সরাসরি join() করে মান নিতে পারেন কারণ তারা ইতিমধ্যে সম্পন্ন। anyOf একটি Object ফিউচার ফেরত দেয়, তাই টাইপ কাস্টিংয়ের প্রয়োজন হতে পারে।

৬. কাস্টম এক্সিকিউটর ব্যবহার

ডিফল্টভাবে supplyAsync এবং runAsync সাধারণ ForkJoinPool.commonPool() ব্যবহার করে, যা CPU-intensive কাজের জন্য ভালো। কিন্তু I/O বা ব্লকিং কাজের জন্য নিজস্ব থ্রেড পুল দেওয়া উচিত।

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

ExecutorService executor = Executors.newFixedThreadPool(5);

CompletableFuture<String> ioTask = CompletableFuture.supplyAsync(() -> {
    // ডাটাবেস কল বা HTTP রিকোয়েস্ট সিমুলেশন
    sleep(2000);
    return "আই/ও ফলাফল";
}, executor);

ioTask.thenAccept(System.out::println);
// ব্যবহার শেষে executor.shutdown();

এতে সাধারণ পুল আটকে না গিয়ে ডেডিকেটেড থ্রেডগুলো কাজ করবে। মনে রাখবেন, executor.shutdown() কল না করলে প্রোগ্রাম থামবে না।

সম্পূর্ণ উদাহরণ

নিচে একটি সম্পূর্ণ রানযোগ্য জাভা ক্লাস দেওয়া হলো যা একটি ই-কমার্স সিমুলেশন করে। এতে একই সাথে ইউজার প্রোফাইল ও অর্ডার হিস্ট্রি আনা হয় এবং একত্রিত করে কনসোলে প্রিন্ট করা হয়। আমরা কাস্টম থ্রেড পুল, চেইনিং, কম্পোজিশন এবং এক্সেপশন হ্যান্ডলিং সবই ব্যবহার করেছি।

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

public class EcommerceAsyncExample {

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

        // ১. ইউজার প্রোফাইল ফেচ (সিমুলেটেড)
        CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
            simulateDelay(1500, "ইউজার সার্ভিস");
            return "ইউজার: রাকিব হোসেন (ID: RH-2024)";
        }, executor);

        // ২. অর্ডার হিস্ট্রি ফেচ (সিমুলেটেড)
        CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> {
            simulateDelay(1200, "অর্ডার সার্ভিস");
            return "অর্ডার: #ORD-1001 (মোট ৳২৩০০)";
        }, executor);

        // ৩. ডিসকাউন্ট সার্ভিস (সিমুলেটেড, মাঝে মাঝে ত্রুটি দেয়)
        CompletableFuture<String> discountFuture = CompletableFuture.supplyAsync(() -> {
            simulateDelay(800, "ডিসকাউন্ট সার্ভিস");
            if (Math.random() > 0.7) {
                throw new RuntimeException("ডিসকাউন্ট সার্ভিস অনুপলব্ধ");
            }
            return "ছাড়: ১৫% প্রযোজ্য";
        }, executor).exceptionally(ex -> {
            System.err.println("সতর্কতা: " + ex.getMessage());
            return "ছাড়: প্রযোজ্য নয় (ফলব্যাক)";
        });

        // ৪. সবগুলো একত্র করা
        CompletableFuture<String> dashboard = userFuture.thenCombine(orderFuture, (user, order) -> {
            return user + " | " + order;
        }).thenCombine(discountFuture, (combined, discount) -> {
            return "=== ড্যাশবোর্ড ===\n" + combined + " | " + discount + "\n==================";
        });

        // ৫. শেষ ফলাফল প্রিন্ট
        dashboard.thenAccept(System.out::println)
                 .thenRun(() -> System.out.println("অ্যাসিঙ্ক্রোনাস ফ্লো সম্পন্ন।"));

        // মেইন থ্রেডকে থামিয়ে রাখা যাতে ফলাফল দেখতে পারি
        try {
            dashboard.get(); // ব্লকিং কল, পুরো ফ্লো শেষ হওয়ার অপেক্ষা
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();
            try {
                if (!executor.awaitTermination(2, TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
            }
        }
    }

    private static void simulateDelay(int millis, String service) {
        try {
            Thread.sleep(millis);
            System.out.println(service + " সম্পন্ন হয়েছে।");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

এই উদাহরণে তিনটি স্বাধীন কাজ আলাদা থ্রেডে চালানো হচ্ছে: ইউজার, অর্ডার, এবং ডিসকাউন্ট। thenCombine দিয়ে প্রথমে ইউজার ও অর্ডার জোড়া লাগিয়ে, তারপর সেই কম্বাইন্ড ভ্যালুর সাথে ডিসকাউন্ট জোড়া হচ্ছে। ডিসকাউন্ট ফেচে যদি ত্রুটি হয়, exceptionally ফলব্যাক মান সরবরাহ করছে, ফলে পুরো প্রোগ্রাম ক্র্যাশ না করে। শেষে thenAccept কনসোলে সম্পূর্ণ ড্যাশবোর্ড দেখায়। মেইন থ্রেড dashboard.get() দিয়ে সব শেষ হওয়ার অপেক্ষা করে। এক্সিকিউটর শাটডাউন না করলে প্রোগ্রাম ঝুলে থাকবে, তাই finally ব্লকে সেটি নিশ্চিত করা হয়েছে।

সাধারণ সমস্যা ও সমাধান

CompletableFuture ব্যবহারের সময় কিছু সাধারণ ভুল দেখা যায়। নিচে সেগুলো এবং তাদের সমাধান দেওয়া হলো।

  • সাধারণ ForkJoinPool-এ ব্লকিং কাজ করা: ডিফল্ট পুলটি মূলত CPU-ইনটেনসিভ কাজের জন্য ডিজাইন করা। যদি আপনি সেখানে Thread.sleep() বা ডাটাবেস কল করেন, তাহলে তা অন্যান্য অ্যাসিঙ্ক্রোনাস কাজকে ধীর করে দিতে পারে। সমাধান: যেকোনো ব্লকিং I/O বা দীর্ঘস্থায়ী কাজের জন্য আলাদা ExecutorService (FixedThreadPool বা CachedThreadPool) ব্যবহার করুন এবং সেটি supplyAsync(..., executor)-এ পাস করুন।
  • get() বা join() খুব তাড়াতাড়ি কল করা: চেইনের শেষ ধাপে বা অবশ্যই প্রয়োজন ছাড়া get() কল করলে থ্রেড ব্লক হয়ে যায়, যা অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের উদ্দেশ্য ব্যর্থ করে। সমাধান: যতটা সম্ভব thenAccept বা thenRun দিয়ে শেষ কাজ শেষ করুন, এবং মেইন থ্রেডের প্রয়োজনে join() ব্যবহার করুন কিন্তু পাইপলাইন শেষেই।
  • এক্সেপশন খেয়ে ফেলা: চেইনে যদি কোনো exceptionally বা handle না থাকে, এবং কোনো স্টেজে এক্সেপশন ঘটে, তাহলে পরবর্তী সব স্টেজ বাদ পড়ে এবং এক্সেপশন চুপচাপ থেকে যায়। প্রোগ্রাম শেষ না হওয়া পর্যন্ত তা জানা যায় না। সমাধান: প্রতিটি ফিউচার চেইনের শেষে exceptionally অথবা handle লাগান, অথবা গ্লোবালি whenComplete দিয়ে লগ করুন।
  • allOf-এর পরেও মান আনতে ভুল করা: অনেকেই allOf(...) কল করে ভাবেন শেষ, কিন্তু তা শুধু Void ফিউচার রিটার্ন করে। তখনও আলাদা আলাদা ফিউচার থেকে join() করতে হয়। সমাধান: allOf(f1,f2).join(); এর পর f1.join() এবং f2.join() আলাদাভাবে কল করুন, অথবা thenCombine দিয়ে আরও সুন্দরভাবে মান বের করুন।
  • থ্রেড পুল শাটডাউন না করা: কাস্টম এক্সিকিউটর ব্যবহার করলে প্রোগ্রামের শেষে executor.shutdown() না দিলে JVM চলতেই থাকবে। সমাধান: shutdown()awaitTermination()-এর সমন্বয়ে সঠিক লাইফসাইকেল নিশ্চিত করুন, অথবা ট্রাই-উইথ-রিসোর্সেস প্যাটার্নে এক্সিকিউটর ব্যবহার করুন (জাভা ১৯ থেকে ExecutorService AutoCloseable হয়েছে)।

সারসংক্ষেপ

এই টিউটোরিয়ালে আমরা জাভার CompletableFuture-এর মৌলিক থেকে অ্যাডভান্সড ব্যবহার শিখলাম। আমরা দেখলাম কীভাবে ফিউচার তৈরি করতে হয়, ট্রান্সফর্ম করতে হয় (thenApply), কনসিউম করতে হয় (thenAccept), কম্পোজ করতে হয় (thenCompose, thenCombine), এক্সেপশন হ্যান্ডেল করতে হয় (exceptionally, handle), এবং একাধিক ফিউচারকে allOfanyOf দিয়ে সমন্বয় করতে হয়। কাস্টম এক্সিকিউটর ব্যবহারের গুরুত্বও বুঝলাম। শেষ সম্পূর্ণ উদাহরণের মাধ্যমে একটি বাস্তবসম্মত অ্যাসিঙ্ক্রোনাস ডেটা ফেচিং ফ্লো বাস্তবায়ন করলাম, যেখানে ত্রুটি থাকলেও প্রোগ্রাম নিরাপদে চলেছে।

CompletableFuture আধুনিক রিঅ্যাকটিভ ও অ্যাসিঙ্ক্রোনাস জাভা অ্যাপ্লিকেশনের ভিত্তি। এখন আপনি একে ব্যবহার করে আপনার কন্ট্রোলার, সার্ভিস লেয়ার এবং ব্যাকগ্রাউন্ড জব আরও কার্যকরীভাবে ডিজাইন করতে পারবেন। পরবর্তী ধাপ হিসেবে আপনি CompletableFuture-এর orTimeout, completeOnTimeout (জাভা ৯+), এবং স্প্রিং @Async-এর সাথে এর সংমিশ্রণ অন্বেষণ করতে পারেন। কোড করতে থাকুন, নতুন কিছু শিখতে থাকুন!

Share