এক নজরে: কে কাকে চালায়

Phase 1-এ তুমি দেখেছ, একটা প্ল্যাটফর্ম থ্রেড সরাসরি OS শিডিউলারের অধীনে থাকে — JVM-এর কোনো নিয়ন্ত্রণ নেই কখন সেটা CPU পাবে। ভার্চুয়াল থ্রেডের গল্পটা সম্পূর্ণ ভিন্ন: এগুলো JVM-এর নিজস্ব শিডিউলার দ্বারা পরিচালিত হয়, আর শেষ পর্যন্ত রান করার জন্য একটা প্ল্যাটফর্ম থ্রেডের উপর "বসে" (mount) থাকে, যাকে বলা হয় ক্যারিয়ার থ্রেড (carrier thread)।

M:N শিডিউলিং মডেল

এটাকে M:N থ্রেডিং মডেল বলা হয় — M সংখ্যক ভার্চুয়াল থ্রেড N সংখ্যক ক্যারিয়ার থ্রেডের উপর মাল্টিপ্লেক্স হয়, যেখানে M সাধারণত N-এর চেয়ে অনেক বড়। ডিফল্টভাবে ক্যারিয়ার থ্রেডের পুলটা একটা ForkJoinPool (work-stealing মোডে), আর এর সাইজ ডিফল্টভাবে Runtime.availableProcessors()-এর সমান — তাই যদি তোমার মেশিনে ৮টা কোর থাকে, ডিফল্টভাবে ৮টা ক্যারিয়ার থ্রেড থাকবে।

// ক্যারিয়ার পুল সাইজ override করা যায় (সাধারণত দরকার হয় না)
// -Djdk.virtualThreadScheduler.parallelism=16

মাউন্ট এবং আনমাউন্ট: এখানেই আসল জাদু

যখন একটা ভার্চুয়াল থ্রেড রান করে, সেটা একটা ক্যারিয়ার থ্রেডে মাউন্ট হয়। যখন সেই ভার্চুয়াল থ্রেড কোনো ব্লকিং অপারেশনে যায় — যেমন Thread.sleep(), একটা Socket রিড, বা ReentrantLock.lock() — তখন JVM সেই ভার্চুয়াল থ্রেডের এক্সিকিউশন স্টেট (একটা continuation) হিপে সংরক্ষণ করে ক্যারিয়ার থ্রেড থেকে সেটাকে আনমাউন্ট করে দেয়। ক্যারিয়ার থ্রেডটা তখন সাথে সাথে অন্য কোনো রেডি ভার্চুয়াল থ্রেড চালাতে মুক্ত হয়ে যায়। ব্লকিং অপারেশন শেষ হলে (যেমন ডাটা এসে পৌঁছালে), ভার্চুয়াল থ্রেডটা আবার কোনো একটা ক্যারিয়ার থ্রেডে (একই থ্রেডে না-ও হতে পারে) রি-মাউন্ট হয় আর চালিয়ে যায়।

Runnable task = () -> {
    System.out.println("Before sleep: " + Thread.currentThread());
    try {
        Thread.sleep(Duration.ofMillis(500)); // এখানে আনমাউন্ট হয়
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    System.out.println("After sleep: " + Thread.currentThread());
};

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(task);
}

এই কোড রান করলে দেখবে — sleep()-এর আগে আর পরে থ্রেডের টুস্ট্রিং একই ভার্চুয়াল থ্রেড আইডি দেখাবে, কিন্তু আন্ডারলাইং ক্যারিয়ার থ্রেড ভিন্ন হতে পারে। জাভা এটা স্বচ্ছভাবে সামলায়, তোমাকে কিছু করতে হয় না।

গ্রিন থ্রেডের সাথে পার্থক্য (কেন এবার এটা কাজ করবে)

Java 1.1-এ একবার "গ্রিন থ্রেড" চেষ্টা করা হয়েছিল আর পরিত্যক্ত হয়েছিল — কারণ সেগুলো শুধু single-core-এ কাজ করত এবং I/O ব্লক করলে পুরো প্রসেস আটকে যেত। ভার্চুয়াল থ্রেড ভিন্ন, কারণ:

  • এগুলো একাধিক ক্যারিয়ার থ্রেডে (তাই একাধিক CPU কোরে) সমান্তরালে চলতে পারে।
  • JDK-এর ভেতরের প্রায় সব ব্লকিং API (socket I/O, BlockingQueue, ReentrantLock, ফাইল I/O) এখন virtual-thread-aware — মানে সত্যিকারের OS-লেভেল ব্লক না করে, তারা ভার্চুয়াল থ্রেডকে আনমাউন্ট করে ক্যারিয়ার থ্রেড ছেড়ে দেয়।

কনটেক্সট সুইচের খরচ কোথায় গেল

Phase 1-এ তুমি দেখেছিলে OS-লেভেল কনটেক্সট সুইচের খরচ — রেজিস্টার সেভ করা, কার্নেল মোডে যাওয়া, ক্যাশ ফ্লাশ হওয়া। ভার্চুয়াল থ্রেডের মাউন্ট/আনমাউন্ট সম্পূর্ণ ইউজার-স্পেসে, JVM-এর ভেতরেই ঘটে — কোনো কার্নেল ট্র্যাপ নেই, কোনো OS শিডিউলার ইনভলভমেন্ট নেই। তাই এই "কনটেক্সট সুইচ" প্ল্যাটফর্ম থ্রেডের তুলনায় বহু গুণ সস্তা, প্রায় একটা মেথড কলের কাছাকাছি খরচ। পরের আর্টিকেলে আমরা দেখব এই মাউন্ট/আনমাউন্ট প্রক্রিয়া কখন ব্যর্থ হয় — অর্থাৎ pinning।

Share