যখন আনমাউন্ট হওয়ার কথা, কিন্তু হয় না
আগের আর্টিকেলে তুমি দেখেছ, ভার্চুয়াল থ্রেড ব্লক করলে সেটা ক্যারিয়ার থ্রেড থেকে আনমাউন্ট হয়ে যায় — এটাই পুরো মডেলের ভিত্তি। কিন্তু কিছু নির্দিষ্ট পরিস্থিতিতে এই আনমাউন্ট হয় না, আর ভার্চুয়াল থ্রেড ক্যারিয়ার থ্রেডকে দখল করেই ব্লকড অবস্থায় বসে থাকে। একে বলা হয় pinning। এটা কোনো বাগ না, বরং JVM-এর একটা সীমাবদ্ধতা যেটা তোমার জানা দরকার।
কারণ ১: synchronized ব্লক বা মেথড
Phase 3-এ তুমি শিখেছিলে synchronized কিভাবে atomic অপারেশন নিশ্চিত করে। কিন্তু ভার্চুয়াল থ্রেড যদি একটা synchronized ব্লকের ভেতরে ব্লক করে (যেমন একটা I/O কল করে), তাহলে সেটা ক্যারিয়ার থ্রেডে পিন হয়ে যায় — কারণ JVM-এর মনিটর লক বাস্তবায়ন সরাসরি OS থ্রেডের সাথে জড়িত, এটা ভার্চুয়াল থ্রেড-অ্যাওয়্যার নয় (Java 21/22-এ)।
// এড়িয়ে চলো: ভার্চুয়াল থ্রেডের সাথে
synchronized (lock) {
String result = restClient.get(url); // পিনড! ক্যারিয়ার থ্রেড আটকে থাকবে
}
// এর বদলে ReentrantLock ব্যবহার করো
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
String result = restClient.get(url); // আনমাউন্ট স্বাভাবিকভাবে কাজ করবে
} finally {
lock.unlock();
}
ReentrantLock (Phase 3-এ পরিচিত) ভার্চুয়াল থ্রেডের সাথে পুরোপুরি সামঞ্জস্যপূর্ণ, কারণ এটা JVM-লেভেলের একটা সাধারণ ক্লাস, OS মনিটরের উপর নির্ভর করে না।
কারণ ২: নেটিভ কোড বা foreign function কল
JNI-এর মাধ্যমে নেটিভ মেথড কল করার সময়, বা কিছু legacy blocking I/O (পুরনো java.io স্ট্রিম, নতুন NIO-based নয়) ব্যবহার করলেও ভার্চুয়াল থ্রেড পিন হতে পারে — কারণ JVM নেটিভ স্ট্যাক ফ্রেমের মাঝখানে থ্রেড আনমাউন্ট করতে পারে না।
পিনিং কিভাবে ডিটেক্ট করবে
JDK একটা সহজ ডায়াগনস্টিক ফ্ল্যাগ দেয়:
-Djdk.tracePinnedThreads=full
এটা চালু করলে, যখনই কোনো ভার্চুয়াল থ্রেড pinned অবস্থায় ব্লক করবে, JVM কনসোলে একটা স্ট্যাক ট্রেস প্রিন্ট করবে দেখানোর জন্য ঠিক কোন লাইনে পিনিং ঘটেছে — সাধারণত একটা synchronized ব্লকের ফ্রেম হাইলাইট করে। এছাড়া JDK Flight Recorder (JFR)-এ jdk.VirtualThreadPinned নামের একটা ইভেন্ট আছে, যেটা প্রোডাকশনে low-overhead মনিটরিং-এর জন্য ব্যবহার করা যায়।
পিনিং কেন খারাপ
পিনিং নিজে ক্র্যাশ করায় না — কিন্তু এটা ভার্চুয়াল থ্রেডের পুরো সুবিধাটা নষ্ট করে দেয়। যদি হাজার হাজার ভার্চুয়াল থ্রেড একটা synchronized ব্লকের ভেতরে ব্লকিং I/O করে, তাহলে প্রতিটা পিনড ভার্চুয়াল থ্রেড একটা করে ক্যারিয়ার থ্রেড দখল করে বসে থাকবে — আর যেহেতু ক্যারিয়ার থ্রেডের সংখ্যা সীমিত (ডিফল্টভাবে CPU কোরের সমান), পুরো অ্যাপ্লিকেশন মূলত আবার প্ল্যাটফর্ম-থ্রেড-স্কেল সীমাবদ্ধতায় ফিরে যায় — বরং আরও খারাপ, কারণ ক্যারিয়ার থ্রেডের সংখ্যা সাধারণ থ্রেড পুলের চেয়েও কম।
ব্যবহারিক পরামর্শ
- হট পাথে (hot path) থাকা synchronized ব্লক, যেগুলোর ভেতরে ব্লকিং I/O কল হয়, সেগুলোকে ReentrantLock-এ কনভার্ট করো।
- থার্ড-পার্টি লাইব্রেরি ব্যবহার করলে (JDBC ড্রাইভার, পুরনো HTTP ক্লায়েন্ট) সেগুলো ইন্টারনালি synchronized ব্যবহার করে কিনা যাচাই করো — অনেক আধুনিক লাইব্রেরি (JDK 21+ সমর্থিত JDBC ড্রাইভার, Java 11+ HttpClient) এই সমস্যা এড়াতে আপডেট হয়েছে।
- মনে রাখা ভালো: JDK 24 (JEP 491)-এ synchronized-জনিত পিনিং সমস্যাটা বড় অংশে সমাধান করা হয়েছে — কিন্তু যতক্ষণ তুমি LTS ভার্সন (Java 21) ব্যবহার করছ, এই সীমাবদ্ধতা মাথায় রাখতে হবে।