একটি ন্যূনতম প্রযোজক-ভোক্তা সেটআপ
কনকারেন্ট কোডে একটি বহুল ব্যবহৃত আকৃতি হলো এক বা একাধিক থ্রেড কাজের আইটেম তৈরি করছে (প্রযোজক) এবং অন্য এক বা একাধিক থ্রেড সেগুলো গ্রহণ করছে (ভোক্তা)। একটি সাধারণ কিউ এবং wait() ও notify() ব্যবহার করে হাতে-কলমে এই সমন্বয় করা সহজেই সূক্ষ্ম ভুলের জন্ম দেয়। BlockingQueue আপনার হয়ে সেই সমন্বয়টি বাস্তবায়ন করে: এটি এমন একটি কিউ যার put কলকারীকে ব্লক করে যখন কিউ পূর্ণ থাকে, এবং take কলকারীকে ব্লক করে যখন কিউ খালি থাকে।
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumer {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10); // সীমাবদ্ধ ধারণক্ষমতা
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 20; i++) {
queue.put(i); // কিউ পূর্ণ থাকলে ব্লক হয়
System.out.println("Produced " + i);
}
queue.put(-1); // ভোক্তাকে থামার সংকেত দেওয়ার জন্য একটি সেন্টিনেল মান
} catch (InterruptedException ignored) {
}
});
Thread consumer = new Thread(() -> {
try {
int item;
while ((item = queue.take()) != -1) { // কিউ খালি থাকলে ব্লক হয়
System.out.println("Consumed " + item);
}
} catch (InterruptedException ignored) {
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
উপরের কোডে প্রযোজক ও ভোক্তা থ্রেডের কোনো লক, কন্ডিশন ভেরিয়েবল বা ম্যানুয়াল নোটিফিকেশনের প্রয়োজন নেই। put() ও take() অভ্যন্তরীণভাবে সমস্ত অপেক্ষা পরিচালনা করে: প্রযোজক যদি ভোক্তার চেয়ে এগিয়ে যায়, তাহলে কিউ তার ১০-এর সীমায় পূর্ণ হলে put() স্বয়ংক্রিয়ভাবে ব্লক হয়ে প্রাকৃতিক ব্যাকপ্রেশার প্রয়োগ করে; আর ভোক্তা যদি এগিয়ে গিয়ে কিউ খালি হয়ে যায়, তাহলে take() প্রযোজক নতুন কিছু যোগ করা পর্যন্ত অপেক্ষা করে। একসময় যে সমন্বয় সতর্ক, সহজেই ভুল হওয়া wait()/notify() কোডের প্রয়োজন হতো, তা এখন একটি পরীক্ষিত স্ট্যান্ডার্ড লাইব্রেরি ক্লাসের ভেতরে সঠিকভাবে সামলানো হয়।
কিউ সীমাবদ্ধ রাখা কেন গুরুত্বপূর্ণ
কোনো ধারণক্ষমতা আর্গুমেন্ট ছাড়া new LinkedBlockingQueue<>() দিয়ে তৈরি আনবাউন্ডেড কিউ ব্যাকপ্রেশার সম্পূর্ণ সরিয়ে দেয়: প্রযোজক যদি ভোক্তার চেয়ে দ্রুত হয়, তাহলে তা অনির্দিষ্টকাল ধরে আইটেম জমা করতে থাকবে এবং মেমরি ব্যবহার প্রক্রিয়ার হিপ শেষ না হওয়া পর্যন্ত বাড়তেই থাকবে। একটি বাউন্ডেড কিউ সেই ব্যর্থতার ধরনকে অনেক বেশি নিয়ন্ত্রণযোগ্য কিছুতে রূপান্তরিত করে: প্রযোজক কেবল ভোক্তার গতির সাথে তাল মিলিয়ে ধীর হয়ে যায়। সীমার আকার নির্ধারণ একটি বাস্তব নকশা-সিদ্ধান্ত, পরের চিন্তা নয়। এটি থ্রুপুট (বড় বাফার ছোট বিস্ফোরণগুলো মসৃণ করে) এবং মেমরি ব্যবহার ও এন্ড-টু-এন্ড ল্যাটেন্সির (ছোট বাফার আইটেমগুলোকে কিউতে বেশিক্ষণ বসিয়ে রাখে না) মধ্যে ভারসাম্য রক্ষা করে।
বাস্তবায়ন নির্বাচন
LinkedBlockingQueue এবং ArrayBlockingQueue হলো সবচেয়ে প্রচলিত দুটি বিকল্প। ArrayBlockingQueue একটি নির্দিষ্ট আকারের ব্যাকিং অ্যারে ব্যবহার করে এবং নির্মাণের সময় অবশ্যই একটি সীমা দিতে হয়; LinkedBlockingQueue সীমাবদ্ধ বা সীমাহীন রাখা যায় এবং উচ্চ প্রতিযোগিতার পরিস্থিতিতে কিছুটা ভালো থ্রুপুট দেয়, কারণ এর লিংকড কাঠামো হেড ও টেইলকে কিছুটা স্বাধীনভাবে পরিচালনা করতে দেয়। SynchronousQueue একটি বিশেষ ধরনের BlockingQueue যার কোনো অভ্যন্তরীণ ধারণক্ষমতা নেই—প্রতিটি put অপারেশন সরাসরি একটি take-এর জন্য অপেক্ষা করে এবং উল্টোটাও সত্য। এটি তখনই উপযোগী যখন আপনি চান প্রযোজক ও ভোক্তা সরাসরি হাতবদল করুক, কোনো বাফারিং ছাড়াই।