জাভাতে একাধিক কাজ সমান্তরালে (concurrently) সম্পন্ন করতে গেলে প্রতিটি কাজের জন্য আলাদা Thread তৈরি করা ধীরে ধীরে অদক্ষ ও ঝুঁকিপূর্ণ হয়ে ওঠে — বিশেষ করে সার্ভার অ্যাপ্লিকেশনে, যেখানে প্রতি মুহূর্তে অসংখ্য সংক্ষিপ্ত কাজ আসতে পারে। java.util.concurrent প্যাকেজের ExecutorService ইন্টারফেসটি থ্রেড ব্যবস্থাপনার এই জটিলতা দূর করে, একটি উচ্চ-স্তরের API প্রদান করে যেখানে আপনি কাজ (Runnable বা Callable) জমা দেন এবং একটি সুসংগঠিত থ্রেড পুল নিজেই সেগুলো সম্পন্ন করে। এই টিউটোরিয়ালে আমরা ExecutorService-এর পেছনের ভাবনা, কনফিগারেশন, টাস্ক জমা দেওয়ার নানান পদ্ধতি, Future ব্যবহার করে ফলাফল গ্রহণ এবং বাস্তব উদাহরণসহ শাটডাউন পদ্ধতি নিয়ে বিস্তারিত আলোচনা করব।

ভূমিকা

প্রথাগতভাবে জাভায় সমান্তরাল প্রক্রিয়াকরণের জন্য আমরা Thread ক্লাস এক্সটেন্ড করতাম অথবা Runnable ইমপ্লিমেন্ট করে থ্রেড তৈরি করতাম। কিন্তু এ পদ্ধতিতে প্রতিটি কাজের জন্য একটি করে থ্রেড অবজেক্ট তৈরি ও ধ্বংস করার খরচ বেশ বড়, এবং অনেক থ্রেড একসঙ্গে চালালে কনটেক্সট সুইচিং ওভারহেডে অ্যাপ্লিকেশন ধীরগতির হয়ে যায়। তাছাড়া থ্রেডের জীবনচক্র ম্যানুয়ালি পরিচালনা করা, ত্রুটি হ্যান্ডলিং ও নিরাপদ শাটডাউন নিশ্চিত করা বেশ কঠিন।

ExecutorService এই সমস্যার একটি মার্জিত সমাধান। এটি থ্রেড পুল ভিত্তিক আর্কিটেকচার প্রদান করে, যেখানে নির্দিষ্ট সংখ্যক থ্রেড বারবার ব্যবহার করা যায় এবং কাজগুলো একটি সারিতে (queue) জমা হয়ে পুলের কোনো ফাঁকা থ্রেড দ্বারা নির্বাহিত হয়। ফলে আপনি কেবল কাজের যুক্তি নির্ধারণ করেন, থ্রেড সৃষ্টি ও ব্যবস্থাপনার কাজটি এক্সিকিউটর ফ্রেমওয়ার্ককে দিয়ে দেন। এছাড়া Future অবজেক্টের মাধ্যমে আপনি কোনো কাজের অগ্রগতি ট্র্যাক করতে, ফলাফল পেতে ও বাতিল করতে পারেন — যা ম্যানুয়াল থ্রেড মডেলে দুরূহ।

জাভা ৫ থেকে java.util.concurrent প্যাকেজে এই ফ্রেমওয়ার্ক যুক্ত হওয়ার পর এটি জাভা কনকারেন্সি প্রোগ্রামিংয়ের মূল ভিত্তি হয়ে দাঁড়িয়েছে। এই বিভাগে আমরা ExecutorService-এর মূল উপাদান, বিভিন্ন ধরণের থ্রেড পুল এবং এর কার্যপদ্ধতি বুঝব।

পূর্বশর্ত

এই টিউটোরিয়ালটি থেকে পূর্ণ সুবিধা পেতে আপনার নিম্নোক্ত বিষয়গুলো সম্পর্কে ধারণা থাকা প্রয়োজন:

  • জাভা প্রোগ্রামিংয়ের মৌলিক জ্ঞান — ক্লাস, ইন্টারফেস, ল্যাম্বডা এক্সপ্রেশন।
  • মাল্টি-থ্রেডিং-এর প্রাথমিক ধারণা: Thread, Runnable, synchronized ব্লক, wait/notify ইত্যাদি।
  • জাভা জেনেরিকস ও Collection ফ্রেমওয়ার্ক সম্পর্কে ধারণা (যেমন List)।
  • JDK ৮ বা তার পরবর্তী সংস্করণ (ল্যাম্বডা সুবিধার জন্য)।
  • যেকোনো জাভা IDE বা টেক্সট এডিটর ও টার্মিনাল।

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

১. ExecutorService তৈরি করা

ExecutorService ইন্টারফেসের ইন্সট্যান্স পাওয়ার সবচেয়ে সহজ উপায় হলো Executors ইউটিলিটি ক্লাসের ফ্যাক্টরি মেথড ব্যবহার করা। সাধারণত তিন ধরনের পুল ব্যবহৃত হয়:

  • newFixedThreadPool(int n) — নির্দিষ্ট সংখ্যক থ্রেড নিয়ে একটি পুল তৈরি করে। পুলের সব থ্রেড ব্যস্ত থাকলে নতুন কাজ অভ্যন্তরীণ BlockingQueue-তে অপেক্ষা করে।
  • newCachedThreadPool() — প্রয়োজন অনুযায়ী থ্রেড তৈরি করে এবং ৬০ সেকেন্ড অলস থাকলে পুরনো থ্রেড রিসাইকেল করে।
  • newSingleThreadExecutor() — মাত্র একটি থ্রেড নিয়ে পুল, যা কাজগুলো সিরিয়ালি (একের পর এক) সম্পন্ন করে।
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorCreation {
    public static void main(String[] args) {
        ExecutorService fixedPool  = Executors.newFixedThreadPool(4);
        ExecutorService cachedPool = Executors.newCachedThreadPool();
        ExecutorService single     = Executors.newSingleThreadExecutor();

        // পরে শাটডাউন করতে হবে
        fixedPool.shutdown();
        cachedPool.shutdown();
        single.shutdown();
    }
}

উপরের কোডে আমরা তিনটি ভিন্ন এক্সিকিউটর সার্ভিস তৈরি করেছি। fixedPool-এ ৪টি থ্রেড থাকবে এবং একসাথে সর্বোচ্চ ৪টি কাজ চলতে পারবে। cachedPool কোনো নির্দিষ্ট থ্রেড সীমা ছাড়াই নতুন থ্রেড তৈরি করবে এবং single একক থ্রেডে অপারেশন চালাবে। মনে রাখবেন, প্রতিটি ExecutorService ব্যবহারের পর shutdown() ডাকা জরুরি, নইলে JVM থ্রেডগুলোকে শেষ হতে দেবে না।

২. টাস্ক জমা দেওয়া: execute() বনাম submit()

ExecutorService-এ কাজ জমা দেওয়ার জন্য প্রধানত দুটি মেথড আছে:

  • execute(Runnable command) — শুধুমাত্র Runnable গ্রহণ করে, কোনো রিটার্ন ভ্যালু দেয় না এবং কোনো Future ফেরত দেয় না। কাজের ত্রুটি আন-হ্যান্ডেল্ড থাকলে থ্রেড পুলের আচার-ব্যবহারে প্রভাব ফেলতে পারে।
  • submit(Runnable task) / submit(Callable<T> task) — একটি Future অবজেক্ট ফেরত দেয়, যার মাধ্যমে কাজের অবস্থা জানা, ফলাফল পাওয়া এবং ব্যতিক্রম হ্যান্ডেল করা সম্ভব।
ExecutorService executor = Executors.newFixedThreadPool(2);

// execute: Runnable জমা
executor.execute(() -> {
    System.out.println("execute দিয়ে কাজ চলছে: " + Thread.currentThread().getName());
});

// submit: Runnable জমা (Future<?> ফেরত)
Future<?> futureRunnable = executor.submit(() -> {
    System.out.println("submit Runnable: " + Thread.currentThread().getName());
});

// submit: Callable জমা (Future<T> ফেরত)
Future<String> futureCallable = executor.submit(() -> {
    // কিছু গণনা
    return "Callable ফলাফল";
});

executor.shutdown();

এখানে execute দ্রুত কাজের জন্য যথেষ্ট, কিন্তু যখন ফলাফল দরকার বা টাস্ক ব্যর্থ হলে সেটি জানতে চাইলে submitFuture ব্যবহার করা উচিত। Future-র get() মেথড কল করলে কলকারী থ্রেড ব্লক হয়ে কাজ শেষ না হওয়া পর্যন্ত অপেক্ষা করে।

৩. FutureCallable ব্যবস্থাপনা

Callable ইন্টারফেসটি Runnable-এর মতোই, কিন্তু এটি একটি মান রিটার্ন করে এবং চেকড এক্সেপশন থ্রো করতে পারে। submit(Callable<T>) মেথড যে Future<T> দেয়, তা দিয়ে আমরা:

  • boolean isDone() — কাজ শেষ হয়েছে কিনা।
  • T get() — কাজের ফলাফল পেতে (প্রয়োজনে অপেক্ষা করে)।
  • T get(long timeout, TimeUnit unit) — নির্দিষ্ট সময় অপেক্ষা করবে।
  • boolean cancel(boolean mayInterruptIfRunning) — কাজ বাতিল করতে।
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
    Thread.sleep(3000); // দীর্ঘ কাজ
    return 42;
});

System.out.println("কাজ শেষ? " + future.isDone()); // সম্ভবত false
try {
    Integer result = future.get(5, TimeUnit.SECONDS); // ৫ সেকেন্ড অপেক্ষা
    System.out.println("ফলাফল: " + result);
} catch (TimeoutException e) {
    System.out.println("সময় শেষ, কাজ বাতিল করছি");
    future.cancel(true);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
} finally {
    executor.shutdownNow();
}

এই উদাহরণ থেকে বোঝা যায়, Future ব্যবহার করে আমরা একটি অ্যাসিনক্রোনাস কাজের উপরে নিয়ন্ত্রণ রাখতে পারি। TimeoutException হ্যান্ডলিংয়ের মাধ্যমে অ্যাপ্লিকেশনকে অনির্দিষ্টকালের জন্য ঝুলে থাকা থেকে রক্ষা করি।

৪. ExecutorService-এর শাটডাউন

এক্সিকিউটর সার্ভিসকে সঠিকভাবে থামানো অত্যন্ত গুরুত্বপূর্ণ, না হলে JVM প্রক্রিয়া বন্ধ হয় না (নন-ডেমন থ্রেড চলতে থাকে)। শাটডাউনের জন্য ধাপগুলো:

  1. shutdown() — নতুন টাস্ক গ্রহণ বন্ধ করে, তবে জমা থাকা টাস্ক সম্পন্ন হবে।
  2. awaitTermination(long timeout, TimeUnit unit) — সব কাজ শেষ হওয়া বা নির্দিষ্ট সময় পার হওয়া পর্যন্ত অপেক্ষা।
  3. shutdownNow() — জোরপূর্বক সব সচল টাস্ক বাতিল করে এবং অপেক্ষমাণ টাস্ক ফিরিয়ে দেয় (ইন্টারাপ্ট করে)।
ExecutorService executor = Executors.newFixedThreadPool(3);
// ... টাস্ক জমা ...
executor.shutdown();
try {
    if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
        System.out.println("নির্ধারিত সময়ে কাজ শেষ হয়নি, জোরপূর্বক থামানো হচ্ছে");
        List<Runnable> abandoned = executor.shutdownNow();
        System.out.println("বাদ দেওয়া টাস্ক সংখ্যা: " + abandoned.size());
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
    Thread.currentThread().interrupt();
}

উপরের পদ্ধতি অনুসরণ করলে আপনার অ্যাপ্লিকেশন গ্রেসফুলি শাটডাউন হবে এবং কোনো ডেটা লস বা ঝুলন্ত থ্রেড থাকবে না। shutdownNow() কল করার সময় InterruptedException হ্যান্ডেল করা এবং কলকারী থ্রেডের ইন্টারাপ্ট স্ট্যাটাস পুনরায় সেট করা (Thread.currentThread().interrupt()) একটি ভালো প্র্যাকটিস।

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

নিচে একটি পূর্ণাঙ্গ, রানএবল জাভা ক্লাস দেওয়া হলো যা Callable টাস্ক, Future এবং যথাযথ শাটডাউনের ব্যবহার দেখায়। এটি কপি করে আপনার IDE-তে রান করতে পারেন।

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class CallableExample {
    public static void main(String[] args) {
        // 2টি থ্রেডের ফিক্সড পুল
        ExecutorService executor = Executors.newFixedThreadPool(2);
        List<Future<Integer>> futures = new ArrayList<>();

        // ৩টি কলেবল টাস্ক তৈরি ও জমা
        for (int i = 1; i <= 3; i++) {
            final int number = i;
            Callable<Integer> task = () -> {
                // সংখ্যা অনুযায়ী সেকেন্ড অপেক্ষা করে বর্গফল রিটার্ন
                TimeUnit.SECONDS.sleep(number);
                return number * number;
            };
            futures.add(executor.submit(task));
            System.out.println("টাস্ক " + number + " জমা হয়েছে");
        }

        // প্রতিটি Future থেকে ফলাফল সংগ্রহ
        for (int i = 0; i < futures.size(); i++) {
            try {
                Integer result = futures.get(i).get();
                System.out.println("টাস্ক " + (i+1) + " এর ফলাফল: " + result);
            } catch (InterruptedException e) {
                System.out.println("টাস্ক " + (i+1) + " ইন্টারাপ্ট হয়েছে");
                Thread.currentThread().interrupt();
            } catch (ExecutionException e) {
                System.out.println("টাস্ক " + (i+1) + " এক্সিকিউশন ত্রুটি: " + e.getCause());
            }
        }

        // গ্রেসফুল শাটডাউন
        executor.shutdown();
        try {
            if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
        System.out.println("সমস্ত কাজ শেষ, এক্সিকিউটর বন্ধ।");
    }
}

কোডটি একটি ফিক্সড পুল তৈরি করে তিনটি Callable টাস্ক জমা দেয়। প্রতিটি টাস্ক তার ক্রমিক সংখ্যার সমান সেকেন্ড থ্রেড ঘুমিয়ে নিয়ে বর্গ সংখ্যা নির্ণয় করে। submit মেথড Future<Integer> ফেরত দেয়, যা একটি লিস্টে জমা রাখা হয়। পরবর্তী লুপে get() কল করে ফলাফল আনার সময় যদি কোনো ত্রুটি ঘটে, তা দৃষ্টিনন্দনভাবে হ্যান্ডেল করা হয়। সবশেষে shutdown()awaitTermination দিয়ে থ্রেডগুলো নিরাপদে বন্ধ করা হয়। এই প্যাটার্ন বাস্তব অ্যাপ্লিকেশনে ব্যবহার করলে কনকারেন্ট টাস্ক সহজে নিয়ন্ত্রণে থাকবে।

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

ExecutorService ব্যবহারের সময় বেশ কিছু কমন ভুল ও সমস্যা দেখা দেয়। নিচে সেগুলো ও তাদের সমাধান দেওয়া হলো:

  • শাটডাউন না করা (Forgetting shutdown)
    ExecutorService তৈরি করে কাজ জমা দেওয়ার পর shutdown() না ডাকলে থ্রেড পুলের থ্রেডগুলো (ডিফল্ট নন-ডেমন) চিরকাল সক্রিয় থাকে এবং JVM প্রক্রিয়া বন্ধ হয় না। সমাধান: সবসময় finally ব্লকে অথবা ট্রাই-উইথ-রিসোর্স (জাভা ১৯+ এ ExecutorService AutoCloseable) ব্যবহার করে শাটডাউন নিশ্চিত করুন।

  • Future.get() অনির্দিষ্টকাল ব্লক হয়ে থাকা
    get() মেথড টাইম-আউট ছাড়া কল করলে যদি কাজ কখনো শেষ না হয়, আপনার অ্যাপ্লিকেশন স্তব্ধ হয়ে যেতে পারে। সমাধান: সবসময় get(long timeout, TimeUnit unit) ব্যবহার করুন এবং TimeoutException হ্যান্ডেল করে প্রয়োজনে কাজ বাতিল করুন।

  • ভুল থ্রেড পুল কনফিগারেশন
    newFixedThreadPool-এ অনেক বড় সংখ্যা দিলে অতিরিক্ত থ্রেড কনটেক্সট সুইচিং ঘটাবে; আবার প্রয়োজনের তুলনায় কম দিলে কাজ জমা হয়ে বিলম্ব তৈরি হবে। newCachedThreadPool ব্যবহার করলে থ্রেডের সংখ্যা অনিয়ন্ত্রিত বাড়তে পারে, যা মেমরির চাপ সৃষ্টি করতে পারে। সমাধান: আপনার অ্যাপ্লিকেশনের CPU-ইনটেনসিভ নাকি I/O-ইনটেনসিভ কাজ, তার ওপর ভিত্তি করে থ্রেড সংখ্যা নির্বাচন করুন। CPU-ইনটেনসিভের জন্য কোর সংখ্যার সমান বা সামান্য বেশি; I/O-ইনটেনসিভের জন্য বড় সংখ্যা (Wait Time/Service Time ফর্মুলা অনুযায়ী) নির্ধারণ করুন।

  • execute দিয়ে জমা দেওয়ার পর ব্যতিক্রম হারিয়ে যাওয়া
    execute(Runnable)-এ কোনো আনচেকড এক্সেপশন ঘটলে তা থ্রেডের আনকট এক্সেপশন হ্যান্ডলার ছাড়া আর কারো কাছেই ধরা পড়ে না। সমাধান: গুরুত্বপূর্ণ কাজে submit ব্যবহার করে Future-র get() মেথডে এক্সেপশন ক্যাচ করুন অথবা থ্রেড ফ্যাক্টরির মাধ্যমে আনকট এক্সেপশন হ্যান্ডলার সেট করুন।

  • শাটডাউনের পর নতুন টাস্ক জমা দেওয়া
    shutdown() ডাকার পর execute বা submit করলে RejectedExecutionException নিক্ষিপ্ত হয়। সমাধান: লাইফ সাইকেল সুস্পষ্ট করুন — কোনো ফ্ল্যাগ বা অবস্থা দেখে নিশ্চিত হোন এক্সিকিউটর সক্রিয় কিনা।

  • Callable ভুলে Runnable ব্যবহার করা
    Runnable জমা দিলে Future পেলেও তার get() সর্বদা null ফেরত দেয়, ফলে ফলাফল পাওয়া যায় না। সমাধান: যদি কোনো মান ফেরত প্রয়োজন হয়, সবসময় Callable জমা দিন।

সারসংক্ষেপ

ExecutorService জাভা কনকারেন্সি প্রোগ্রামিংয়ের একটি অপরিহার্য উপাদান, যা থ্রেড সরাসরি ব্যবস্থাপনার জটিলতা থেকে মুক্তি দিয়ে সুন্দর API প্রদান করে। আমরা শিখেছি কীভাবে Executors ফ্যাক্টরি দিয়ে বিভিন্ন থ্রেড পুল তৈরি করতে হয়, executesubmit-এর মধ্যে পার্থক্য, FutureCallable-এর মাধ্যমে ফলাফল প্রাপ্তি, এবং সঠিক শাটডাউন পদ্ধতি। একটি সম্পূর্ণ রানএবল উদাহরণের মাধ্যমে আমরা বিষয়গুলো হাতে-কলমে দেখেছি। সাধারণ সমস্যাগুলো চিহ্নিত করে তাদের সমাধান জেনে আপনি এখন বাস্তব প্রকল্পে ExecutorService নির্ভারভাবে ব্যবহার করতে পারবেন। এক্সিকিউটর ফ্রেমওয়ার্কের উন্নততর ব্যবহার যেমন ScheduledExecutorService, CompletionService, ফর্ক/জয়েন পুল ইত্যাদি শেখার জন্য এটি একটি দৃঢ় ভিত্তি হিসেবে কাজ করবে।

Share