ExecutorService ও থ্রেড পুল: নিজে থ্রেড তৈরি করা বন্ধ করুন
এই সিরিজের প্রথম পর্বে দেখানো হয়েছে যে প্রতিটি OS থ্রেড প্রকৃত মেমোরি সংরক্ষণ করে এবং কনটেক্সট-সুইচিংয়ে বাস্তব ওভারহেড যোগ করে, আর হাজার হাজার থ্রেড তৈরি করলে অপারেটিং সিস্টেমের সম্পদ শেষ হয়ে যেতে পারে। ExecutorService-এর কাজই হলো সেই সীমা নির্ধারণ করা: একটি নির্দিষ্ট, পুনরায় ব্যবহারযোগ্য ওয়ার্কার থ্রেডের পুল, যেখানে প্রতিটি কাজের জন্য নতুন Thread না বানিয়ে কাজ জমা দেওয়া হয়।
নতুন থ্রেড চালু না করে কাজ জমা দেওয়া
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorBasics {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 1; i <= 10; i++) {
int taskId = i;
executor.execute(() ->
System.out.println("Task " + taskId + " on " + Thread.currentThread().getName()));
}
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
}
}
দশটি কাজ মাত্র চারটি পুনরায় ব্যবহারযোগ্য থ্রেডের ওপর চলে, আপনি যত কাজই জমা দিন না কেন। execute() সঙ্গে সঙ্গে রিটার্ন করে; এক্সিকিউটর কাজটিকে কিউতে রাখে এবং পুলের যে থ্রেডটি আগে ফ্রি হয় তাকে দিয়ে দেয়। থ্রেডগুলো একবারই তৈরি হয়, শুরুতেই, এবং পরবর্তী প্রতিটি কাজের জন্য পুনর্ব্যবহৃত হয় — এটাই সেই নির্দিষ্ট ও সীমাবদ্ধ খরচ যা new Thread()-এর অসীম খরচের বদলে এই API দেয়।
Callable ও Future দিয়ে ফলাফল ফেরত আনা
execute() কোনো মান ফেরত দিতে পারে না, চেক্ড এক্সেপশনও ছড়াতে পারে না। submit() একটি Callable-এর সাথে Future-এর মাধ্যমে তা পারে:
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Integer> future = executor.submit(() -> {
Thread.sleep(200);
return 21 * 2;
});
System.out.println("জমা দেওয়া হয়েছে, অন্য কাজ চলছে...");
Integer result = future.get(); // কাজ শেষ না হওয়া পর্যন্ত ব্লক করে
System.out.println("ফলাফল: " + result);
executor.shutdown();
}
}
future.get() কলিং থ্রেডকে ততক্ষণ ব্লক করে রাখে যতক্ষণ না কাজ শেষ হয়, তারপর ফলাফল রিটার্ন করে অথবা ব্যতিক্রম থাকলে সেটিকে ExecutionException-এ মুড়ে পুনরায় নিক্ষেপ করে। প্রোডাকশন কোডে future.get(timeout, unit) ব্যবহার করাই ভালো, যাতে কোনো আটকে যাওয়া কাজ কলারকে চিরতরে ব্লক না রাখে।
সঠিকভাবে শাটডাউন করা
ExecutorService-এর থ্রেডগুলো নিজে থেকে থামে না; শাটডাউন করতে ভুলে গেলে পুলের থ্রেডগুলো অনির্দিষ্টকাল চলতে থাকে। প্রথম পর্বে দেখা ডেমন থ্রেডের বিপরীতে এগুলো সাধারণ ইউজার থ্রেড, তাই JVM-কে বাঁচিয়ে রাখবে:
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
// এখানে কাজ জমা দিন
} finally {
executor.shutdown(); // নতুন কাজ নেওয়া বন্ধ করে
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
executor.shutdownNow(); // এখনও চলমান কাজগুলোতে বাধা দেয়
}
}
shutdown() ইতিমধ্যে জমা দেওয়া কাজগুলো শেষ হতে দেয় কিন্তু নতুন কাজ আর গ্রহণ করে না; awaitTermination নির্দিষ্ট সময় পর্যন্ত অপেক্ষা করে। সময় পেরিয়ে গেলে shutdownNow() চলমান থ্রেডগুলোতে ইন্টারাপ্ট পাঠিয়ে দ্রুত বন্ধ করে।