এই পর্বে যা শিখবেন
- thread-per-request মডেল কেন সহজ, আর কেন কয়েক হাজার request-এ এসে আটকে যায়
- async/reactive সমাধানের দাম, আর virtual thread কীভাবে সহজ কোড ফিরিয়ে দেয়
- carrier thread-এ mount, ব্লকিং-এ unmount, heap-এ continuation — ভেতরের যন্ত্রটা ধাপে ধাপে
- virtual thread তৈরির উপায়, কোথায় লাভ আর কোথায় নেই, pinning, Semaphore, ThreadLocal আর ScopedValue
- বাস্তব প্রজেক্টে চালু করা — Spring Boot, thread dump, আর migration-এর চেকলিস্ট
১ · এক request, এক thread — সহজ মডেল আর তার দেয়াল
পর্ব ৪০-এ আপনি thread দেখেছেন — একটি প্রোগ্রামের ভেতরে একসাথে চলা আলাদা কাজের ধারা। পর্ব ৪১-এ দেখেছেন thread pool আর ExecutorService, যেখানে কয়েকটি thread বারবার কাজ নেয়। এবার একটি অনলাইন দোকানের সার্ভার ভাবুন। ঈদের আগের রাতে একসাথে দশ হাজার ক্রেতা “আমার কার্ট দেখাও” চাপলেন। প্রতিটি request-এর কাজ মোটামুটি এমন:
Cart handle(Request req) {
User user = userDb.find(req.userId()); // ডেটাবেসের উত্তরের অপেক্ষা
Cart cart = cartDb.load(user.id()); // আবার অপেক্ষা
Price price = priceApi.fetch(cart.items()); // অন্য সার্ভারের অপেক্ষা
return cart.withPrice(price);
}কোডটি ওপর থেকে নিচে পড়া যায়, ভুল হলে stack trace-এ ঠিক লাইনটি দেখা যায়, ডিবাগারে ধাপে ধাপে চালানো যায়। প্রতিটি request-কে একটি আলাদা thread দিলে এই সহজ কোডই যথেষ্ট — একে বলে thread-per-request মডেল। বহু বছর ধরে Java সার্ভার (Tomcat, Jetty) এভাবেই চলেছে।
সমস্যা লুকিয়ে আছে মন্তব্যগুলোতে: “অপেক্ষা”। একটি সাধারণ request হয়তো সব মিলিয়ে ১০০ মিলিসেকেন্ড নেয়, কিন্তু তার মধ্যে CPU সত্যিই কাজ করে হয়তো ১-২ মিলিসেকেন্ড। বাকিটা thread বসে থাকে — ডেটাবেসের উত্তর, নেটওয়ার্কের প্যাকেট। এই অপেক্ষাকে বলে blocking I/O: thread ঘুমিয়ে থাকে, কিন্তু নিজের জায়গা ছাড়ে না।
একটি সরকারি অফিসের কাউন্টার ভাবুন। প্রতিটি আবেদনকারীর জন্য একজন করে কর্মী, আর প্রতিটি আবেদনে অন্য দপ্তর থেকে একটি সই আনাতে হয়। কর্মী ফাইল পাঠিয়ে চেয়ারে বসে থাকেন — সই না আসা পর্যন্ত অন্য কারও কাজ ধরেন না। অফিসে চেয়ার ২০০টি। ২০১তম আবেদনকারী দাঁড়িয়ে থাকেন, যদিও ভেতরের কর্মীরা প্রায় কেউই আসলে কাজ করছেন না — সবাই সইয়ের অপেক্ষায়। চেয়ার বাড়ানো যায়, কিন্তু প্রতিটি চেয়ার, টেবিল আর বেতন দামি। পুরোনো Java thread হলো এই কর্মী।
কেন thread বাড়ালেই হয় না
Java 1.2-1.3-এর সময় থেকে প্রতিটি সাধারণ java.lang.Thread আসলে অপারেটিং সিস্টেমের একটি OS thread-এর পাতলা মোড়ক — এক Java thread, এক OS thread. Java 21-এ এদের নাম দেওয়া হয়েছে platform thread. প্রতিটির খরচ:
- বড় stack: প্রতিটি thread-এর নিজের call stack লাগে। 64-bit Linux বা Windows-এ ডিফল্টে সাধারণত প্রায় ১ MB জায়গা সংরক্ষিত (reserve) থাকে (
-Xssদিয়ে বদলানো যায়)। পুরোটা সাথে সাথে আসল RAM খায় না, কিন্তু ঠিকানার জায়গা আর OS-এর হিসাব আটকে রাখে। - তৈরি ও ধ্বংসে খরচ: প্রতিবার OS-এর কাছে আবেদন — এ জন্যই পর্ব ৪১-এ thread pool দিয়ে thread বারবার ব্যবহার করা হয়েছিল।
- OS-এর scheduler-এর ভার: হাজার হাজার thread-এর মধ্যে CPU ভাগ করতে context switch লাগে, যা নিজেই সময় খায়।
- সীমা: একটি সাধারণ সার্ভারে কয়েক হাজার thread-এর পর মেমরি বা OS-এর সীমায় পৌঁছানো যায়; তখন নতুন thread তৈরি করতে গেলে OutOfMemoryError আসে (বার্তায় বলা থাকে native thread তৈরি করা যায়নি)।
ফল: সার্ভারের CPU হয়তো ৫% ব্যস্ত, অথচ thread শেষ — নতুন ক্রেতা অপেক্ষায়। একে বলে thread-এর সংখ্যাই throughput-এর সীমা। সার্ভার কতগুলো request প্রতি সেকেন্ডে সামলাবে, তা ঠিক করছে CPU নয়, একসাথে কতগুলো thread রাখা যায় তা।
পর্ব ৩২-এ দেখা Little's law এখানে সরাসরি খাটে: একসাথে চলা request (L) = প্রতি সেকেন্ডে আসা request (λ) × প্রতিটির সময় (W). উল্টে লিখলে throughput λ = L ÷ W. প্রতিটি request ১০০ মিলিসেকেন্ড (০.১ সেকেন্ড) নিলে আর thread ২০০টি হলে সর্বোচ্চ ২০০ ÷ ০.১ = ২,০০০ request প্রতি সেকেন্ডে। ১০,০০০ request প্রতি সেকেন্ডে চাইলে একসাথে ১,০০০টি request চলতে হবে — অর্থাৎ ১,০০০টি thread. দরকার অনেক বেশি একসাথের অপেক্ষা, আর অপেক্ষা সস্তা হওয়া উচিত।
২ · async আর reactive — সমাধান, কিন্তু চড়া দামে
thread কম, অপেক্ষা বেশি — এই সমস্যার প্রথম সমাধান ছিল: অপেক্ষার সময় thread-কে বসিয়ে রাখব না। ডেটাবেসে প্রশ্ন পাঠিয়ে thread ফিরে যাবে অন্য কাজে, আর উত্তর এলে একটি callback চালানো হবে। পর্ব ৪১-এর CompletableFuture ঠিক এই ধাঁচ:
CompletableFuture<Cart> handle(Request req) {
return userDb.findAsync(req.userId())
.thenCompose(user -> cartDb.loadAsync(user.id()))
.thenCompose(cart -> priceApi.fetchAsync(cart.items())
.thenApply(cart::withPrice))
.exceptionally(e -> Cart.empty());
}এভাবে অল্প কয়েকটি thread দিয়ে লাখো সংযোগ সামলানো যায় — Netty, Vert.x, Spring WebFlux-এর মতো reactive programming framework এই নীতিতেই বানানো। কিন্তু দাম আছে:
| বিষয় | ব্লকিং কোড (thread-per-request) | async / reactive কোড |
|---|---|---|
| পড়া | ওপর থেকে নিচে, সাধারণ if, for, try | lambda-র শিকল; লুপ আর শর্ত লেখা কঠিন |
| Exception | সাধারণ try-catch | exceptionally, handle — আলাদা নিয়ম |
| Stack trace | কোন method কাকে ডেকেছে, পুরো পথ দেখা যায় | শুধু library-র ভেতরের কিছু ফ্রেম; আসল ডাকার পথ হারিয়ে যায় |
| ডিবাগার | ধাপে ধাপে চলে | একটি ধাপ এক thread-এ, পরেরটা আরেক thread-এ — ধাপে ধাপে চালানো প্রায় অসম্ভব |
| সংক্রমণ | — | একটি method async হলে তাকে যে ডাকে সেটাকেও async হতে হয় — পুরো কোডবেস বদলায় |
| scale | thread-এর সংখ্যায় আটকে যায় | খুব ভালো |
অর্থাৎ বাছাই ছিল দুটির একটি: সহজ কোড কিন্তু কম scale, অথবা বড় scale কিন্তু কঠিন কোড। Virtual thread-এর লক্ষ্য দুটোই একসাথে দেওয়া — ব্লকিং কোড লিখুন, scale পাবেন async-এর মতো।
৩ · virtual thread কী — carrier, mount আর unmount
Virtual thread হলো এমন একটি Thread, যাকে OS চেনে না — JVM নিজে চালায়। Java 19-এ preview হিসেবে আসে (JEP 425), Java 20-এ দ্বিতীয় preview, আর Java 21 (সেপ্টেম্বর ২০২৩)-এ স্থায়ী হয় JEP 444-এর মাধ্যমে। কোনো flag লাগে না।
মূল কৌশল: virtual thread নিজে কোনো CPU-তে চলে না। চলার সময় সে একটি সাধারণ platform thread-এর পিঠে চড়ে — সেই platform thread-কে বলে carrier thread. Carrier-গুলো একটি ForkJoinPool-এর thread, ডিফল্টে সংখ্যায় মোটামুটি CPU core-এর সমান। একটি virtual thread যখন ব্লকিং কাজে পৌঁছায় — Thread.sleep, socket থেকে পড়া, java.util.concurrent-এর lock বা queue-তে অপেক্ষা — তখন JVM তাকে carrier থেকে নামিয়ে দেয়। তার চলমান অবস্থা, অর্থাৎ stack-এর ফ্রেমগুলো, একটি continuation object হিসেবে heap-এ রাখা হয়। Carrier মুক্ত, সে আরেকটি অপেক্ষমাণ virtual thread তুলে নেয়।
- প্রস্তুত সারি
A B C D …১০,০০০ virtual thread - carrier
C1, C2core-এর সমান - heap
continuationঅপেক্ষার সময় stack এখানে - I/O শেষ
আবার সারিতেযেকোনো carrier-এ
প্রস্তুত সারি: A B C D … (১০,০০০টি)
carrier C1: খালি carrier C2: খালি
ধরা যাক মেশিনে ২টি core — তাই carrier ২টি
C1 ← A (A-র কোড চলছে)
C2 ← B (B-র কোড চলছে)
সারি: C D E …
virtual thread চলে শুধু carrier-এর পিঠে চড়ে
A: cartDb.load(...) → socket থেকে পড়া — উত্তর এখনো আসেনি
A-র stack ফ্রেম ──► heap-এ continuation
C1: খালি!
A নেমে গেল (unmount) — OS thread ঘুমায়নি
C1 ← C (সারির পরেরটি)
C2 ← B (এখনো চলছে)
heap: A (অপেক্ষায়)
কোনো carrier অলস বসে নেই
OS জানাল: A-র socket-এ ডেটা এসেছে
A → আবার প্রস্তুত সারিতে
সারি: D E … A
A এখনো heap-এ, কোনো carrier-এ নয়
B শেষ হলো → C2 ← A
A-র stack heap থেকে ফেরত, পরের লাইন থেকে চলা শুরু
A-র কাছে মনে হলো load() শুধু একটু সময় নিয়েছে
- অনেক virtual thread, অল্প carrier. ১০,০০০টি virtual thread তৈরি হয়েছে, কিন্তু চালানোর মতো carrier মাত্র ২টি — core-এর সংখ্যার সমান। virtual thread-গুলো scheduler-এর সারিতে অপেক্ষা করে।
- Mount. Scheduler A-কে C1-এ আর B-কে C2-তে বসায়। এখন A-র কোড সত্যিই CPU-তে চলছে, একটি OS thread-এর মাধ্যমে। একে বলে mount হওয়া।
- ব্লকিং — unmount. A ডেটাবেসের উত্তর চায়, উত্তর এখনো আসেনি। platform thread হলে এখানে পুরো OS thread ঘুমিয়ে পড়ত। Virtual thread হওয়ায় JVM A-র stack ফ্রেমগুলো heap-এ সরিয়ে রাখে আর A-কে নামিয়ে দেয়। C1 মুক্ত।
- Carrier কখনো অলস নয়। C1 সাথে সাথে সারি থেকে C-কে তুলে নেয়। এভাবে ২টি carrier দিয়েই হাজারো অপেক্ষমাণ কাজ চলে, কারণ যেকোনো মুহূর্তে বেশিরভাগ কাজ অপেক্ষায়, চলায় নয়।
- উত্তর এলে আবার সারিতে। JDK-র ভেতরের একটি poller OS-এর কাছ থেকে খবর পায় যে A-র socket-এ ডেটা এসেছে। A আবার প্রস্তুত সারিতে ঢোকে।
- আবার mount, অন্য carrier-এও হতে পারে। যে carrier আগে মুক্ত হয়, সে-ই A-কে নেয় — এবার C2. A-র stack ফেরত আসে, আর
load()-এর পরের লাইন থেকে চলা শুরু। A-র কোডের কাছে পুরো ঘটনাটা অদৃশ্য: তার কাছে মনে হয় একটি সাধারণ ব্লকিং ডাক একটু সময় নিয়েছে।
এই পুরো নকশাকে বলে M:N scheduling: M সংখ্যক virtual thread চলে N সংখ্যক OS thread-এর ওপর, যেখানে M অনেক বড়। Scheduler-টি পর্ব ৩৫-এর parallel stream-এর মতোই work stealing করে, তবে এখানে FIFO ক্রমে।
Virtual thread দ্রুত নয়, সস্তা। একটি virtual thread একটি platform thread-এর চেয়ে কোড দ্রুত চালায় না — একই CPU, একই নির্দেশ। লাভটা সংখ্যায়: একসাথে অনেক বেশি অপেক্ষমাণ কাজ রাখা যায়। JEP 444-এর ভাষায় এরা latency কমায় না, throughput বাড়ায়।
৪ · মেমরির ছবি — ১০,০০০ platform বনাম ১০,০০০ virtual
সংখ্যাগুলো পাশাপাশি রাখলে পার্থক্যটা স্পষ্ট হয়। নিচের সংখ্যাগুলো মোটা দাগের — আসল মান OS, JDK সংস্করণ আর stack-এর গভীরতার ওপর নির্ভর করে।
- ১ platform
~১ MB stackOS thread - ১০,০০০ platform
~১০ GB reserveসীমার কাছে - ১ virtual
heap-এ objectদরকার মতো বাড়ে - ১০,০০০ virtual
কয়েক MB থেকে কয়েক দশ MBcarrier = core
Java Thread ──► OS thread (kernel জানে)
stack: আগে থেকেই ~1 MB জায়গা সংরক্ষিত (-Xss)
তৈরি: OS-এর কাছে system call
একটি-দুটিতে কোনো সমস্যা নেই
10,000 × ~1 MB ≈ ~10 GB ঠিকানার জায়গা সংরক্ষিত OS scheduler: ১০,০০০ thread-এর মধ্যে core ভাগ বেশিরভাগ ঘুমিয়ে, তবু প্রতিটি জায়গা আটকে রেখেছে OS-এর thread-সীমায় পৌঁছালে OutOfMemoryError
Java Thread object (heap-এ) + stack chunk (heap-এ)
শুরুতে খুব ছোট; call stack গভীর হলে বাড়ে, ছোট হলে কমে
তৈরি: সাধারণ object তৈরির মতো — OS জানেই না
GC অন্য object-এর মতোই এদের মেমরি ফেরত নেয়
10,000 × (কয়েকশো byte থেকে কয়েক KB) → সব মিলিয়ে সাধারণত কয়েক MB
carrier: মাত্র core-এর সমান সংখ্যক OS thread
১০ লাখ virtual thread-ও সাধারণ মেশিনে সম্ভব
- একটি platform thread. প্রতিটি সাধারণ Java thread-এর পেছনে একটি OS thread, আর তার stack-এর জন্য আগে থেকে একটি বড় জায়গা সংরক্ষিত। তৈরির সময় OS-এর কাছে আবেদন করতে হয়।
- দশ হাজার platform thread. শুধু stack-এর জন্যই প্রায় ১০ GB ঠিকানার জায়গা সংরক্ষিত, আর OS-কে দশ হাজার thread-এর হিসাব রাখতে হয়। বেশিরভাগ thread ঘুমিয়ে আছে, তবু প্রতিটি নিজের জায়গা ধরে রেখেছে। অনেক সার্ভারে এর আগেই OS বা কন্টেইনারের সীমায় পৌঁছানো যায়।
- একটি virtual thread. এটি heap-এর একটি সাধারণ object. তার stack-ও heap-এ, ছোট টুকরো হিসেবে, যতটুকু দরকার ততটুকুই। তৈরি করতে OS-কে কিছু বলতে হয় না, তাই খরচ প্রায় একটি object তৈরির মতো।
- দশ হাজার virtual thread. মোট মেমরি সাধারণত কয়েক MB, আর OS দেখে মাত্র কয়েকটি carrier thread. এ জন্যই JEP 444 বলে, virtual thread এত সস্তা যে এদের pool করার দরকার নেই — প্রতিটি কাজের জন্য নতুন একটি তৈরি করুন।
২০০টি platform thread-এর pool বনাম virtual thread — ১০,০০০টি অপেক্ষার কাজ
JEP 444-এ নিজেই এই পরীক্ষাটি আছে। ১০,০০০টি কাজ, প্রতিটি ১ সেকেন্ড ঘুমায় (ডেটাবেসের অপেক্ষার ভান)। পর্ব ৪১-এর মতো Executors.newFixedThreadPool(200) দিলে একসাথে চলে মাত্র ২০০টি, তাই ১০,০০০ ÷ ২০০ = ৫০ দফা — মোট সময় মোটামুটি ৫০ সেকেন্ড, অর্থাৎ প্রতি সেকেন্ডে প্রায় ২০০টি কাজ। একই কোডে শুধু executor বদলে Executors.newVirtualThreadPerTaskExecutor() দিলে সব ১০,০০০টি একসাথে ঘুমায় — মোট সময় মোটামুটি ১ সেকেন্ডের কাছাকাছি (সাথে কিছু বাড়তি খরচ)। কোডের আর একটি অক্ষরও বদলায়নি। এটাই Little's law: W একই রইল, L বাড়ল ৫০ গুণ, তাই throughput-ও প্রায় ৫০ গুণ।
৫ · virtual thread তৈরি — চারটি উপায়, একটি পছন্দের
| উপায় | কোড | কখন |
|---|---|---|
| সরাসরি | Thread.startVirtualThread(task) | একটি কাজ, দ্রুত |
| Builder | Thread.ofVirtual().name("sms-", 0).start(task) | নাম (ক্রমিক নম্বরসহ) বা অন্য সেটিং দরকার হলে |
| ThreadFactory | Thread.ofVirtual().factory() | যে library একটি ThreadFactory চায় |
| Executor (সবচেয়ে কাজের) | Executors.newVirtualThreadPerTaskExecutor() | অনেক কাজ, ফল ফেরত চাই — প্রতি কাজে নতুন virtual thread |
Thread.ofVirtual() একটি Thread.Builder দেয়; একই ধাঁচে Thread.ofPlatform() দিয়ে platform thread-ও বানানো যায়। Virtual thread সবসময় daemon thread — অর্থাৎ শুধু virtual thread বাকি থাকলে JVM তাদের জন্য অপেক্ষা না করেই বন্ধ হতে পারে। তাই ফল দরকার হলে join() করুন বা executor ব্যবহার করুন। এদের priority-ও বদলানো যায় না। এবার সেই দশ হাজার ক্রেতার সার্ভার, ছোট আকারে:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
class Main {
static int askDatabase(int requestId) throws InterruptedException {
Thread.sleep(10); // ডেটাবেসের উত্তরের অপেক্ষা (ভান)
return requestId % 7; // ধরা যাক, কার্টে কয়টি পণ্য
}
public static void main(String[] args) throws Exception {
Thread one = Thread.ofVirtual().start(() -> System.out.println("একটি virtual thread চলছে"));
one.join();
System.out.println("isVirtual = " + one.isVirtual());
List<Future<Integer>> results = new ArrayList<>();
try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int id = i;
results.add(pool.submit(() -> askDatabase(id))); // প্রতি request-এ নতুন virtual thread
}
} // close() সব কাজ শেষ হওয়া পর্যন্ত অপেক্ষা করে
int total = 0;
for (Future<Integer> f : results) total += f.get(); // জমা দেওয়ার ক্রমেই
System.out.println(results.size() + "টি request শেষ, মোট পণ্য = " + total);
}
}আউটপুট তিন লাইন: একটি virtual thread চলছে, isVirtual = true, আর 10000টি request শেষ, মোট পণ্য = 29994. প্রথম অংশে Thread.ofVirtual().start(...) একটি virtual thread চালু করে, আর join() তার শেষ হওয়ার অপেক্ষা করে — ঠিক platform thread-এর মতো। isVirtual() (Java 21 থেকে) বলে দেয় কোন জাতের thread. দ্বিতীয় অংশে ১০,০০০টি কাজ, প্রতিটি ১০ মিলিসেকেন্ড ঘুমায়। একটি একটি করে চালালে লাগত প্রায় ১০০ সেকেন্ড; এখানে সবাই একসাথে ঘুমায়, তাই পুরো প্রোগ্রাম অনলাইন runner-এও দ্রুত শেষ হয়। Future-গুলো জমা দেওয়ার ক্রমে একটি তালিকায় রাখা, আর ফল যোগ করা হয়েছে সেই ক্রমেই — তাই আউটপুট প্রতিবার এক। কোন কাজ আগে শেষ হলো বা কোন carrier-এ চলল, তা আমরা ইচ্ছা করেই ছাপিনি; সেটা প্রতিবার বদলায়। খেয়াল করুন: try-with-resources-এর শেষে close() নিজেই সব কাজ শেষ হওয়ার অপেক্ষা করে (Java 19 থেকে ExecutorService একটি AutoCloseable). (অনলাইন runner ফাইলটিকে অন্য নামে compile করে, তাই Main এখানে public নয়; নিজের প্রজেক্টে public class Main থাকে Main.java-তে।)
বাস্তব উদাহরণ — ধীর API থেকে ১,০০০ পণ্যের দাম
একটি তুলনামূলক দামের সাইট ১,০০০টি পণ্যের দাম একটি সরবরাহকারীর API থেকে আনে; প্রতিটি ডাকে প্রায় আধা সেকেন্ড লাগে। একটি একটি করে আনলে প্রায় ৫০০ সেকেন্ড। Virtual thread দিয়ে লেখা একেবারে সাধারণ ব্লকিং কোড (এই অংশটি অনলাইনে চালানো যাবে না — বাইরের নেটওয়ার্ক নেই):
HttpClient http = HttpClient.newHttpClient();
List<Future<String>> prices = new ArrayList<>();
try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
for (String id : productIds) { // ১,০০০টি পণ্য
var req = HttpRequest.newBuilder(URI.create(API + id)).build();
prices.add(pool.submit(() ->
http.send(req, HttpResponse.BodyHandlers.ofString()).body())); // ব্লকিং send — কোনো সমস্যা নেই
}
}
// এখানে সব দাম হাতে; prices.get(i).resultNow() — productIds-এর ক্রমেইকোনো callback নেই, কোনো thenCompose নেই — শুধু http.send, যা উত্তর না আসা পর্যন্ত আটকে থাকে। কিন্তু আটকে থাকে virtual thread, carrier নয়। মোট সময় প্রায় সবচেয়ে ধীর একটি ডাকের সমান — যদি সরবরাহকারী একসাথে ১,০০০টি ডাক মেনে নেয়। না নিলে? সেটাই অংশ ৭-এর Semaphore-এর গল্প।
৬ · কোথায় কাজে লাগে, কোথায় লাগে না
I/O-bound — virtual thread-এর আসল জায়গা
I/O-bound কাজ: বেশিরভাগ সময় অপেক্ষা — ওয়েব সার্ভারের request, ডেটাবেস query, অন্য সার্ভিসের HTTP ডাক, ফাইল বা message queue. একসাথে হাজারো এমন কাজ থাকলে বিশাল লাভ।
অনেক কাজ + বেশিরভাগ সময় অপেক্ষাCPU-bound — কোনো বাড়তি লাভ নেই
CPU-bound কাজ: ছবি প্রক্রিয়া, বড় হিসাব, এনক্রিপশন। এখানে CPU সারাক্ষণ ব্যস্ত; ৮টি core থাকলে একসাথে ৮টির বেশি কাজ সত্যিই চলতে পারে না। ১০,০০০ virtual thread দিলেও carrier ৮টিই।
core-এর সংখ্যাই সীমাঅল্প কাজ — পার্থক্য চোখে পড়বে না
একসাথে ২০-৫০টি কাজ হলে সাধারণ thread pool-ও ঠিক আছে। Virtual thread-এর লাভ দেখা দেয় যখন একসাথের কাজের সংখ্যা হাজারের ঘরে।
লাভ আসে সংখ্যা থেকেCPU-ভারী কাজের জন্য আগের সমাধানই ঠিক: core-এর সমান thread-এর একটি pool, অথবা পর্ব ৩৫-এর parallel stream বা ForkJoinPool. বরং virtual thread-এ লম্বা CPU-ভারী কাজ দিলে একটি সূক্ষ্ম সমস্যা হয়: Java 21-এর scheduler virtual thread-কে জোর করে থামায় না (time slicing নেই); একটি virtual thread নিজে ব্লক না করা পর্যন্ত carrier ছাড়ে না। কয়েকটি এমন কাজ সব carrier দখল করে রাখলে হাজারো ছোট I/O কাজ সারিতে আটকে থাকে।
ফাইল I/O-র একটি ব্যতিক্রম। নেটওয়ার্ক socket-এর অপেক্ষায় virtual thread পরিষ্কারভাবে নেমে যায়। কিন্তু অনেক OS-এ ফাইল পড়া-লেখার কিছু কাজ এভাবে ভাগ করা যায় না; সেখানে JDK সাময়িকভাবে একটি বাড়তি carrier যোগ করে ক্ষতিপূরণ দেয়। তাই ফাইল-ভারী কাজেও চলে, তবে নেটওয়ার্কের মতো নিখুঁত নয়।
৭ · pinning, Semaphore আর ভালো অভ্যাস
Pinning — যখন virtual thread নামতে পারে না
কিছু অবস্থায় ব্লক করার সময় virtual thread carrier থেকে নামতে পারে না; carrier-ও তার সাথে আটকে থাকে। একে বলে pinning. Java 21-এ দুটি প্রধান কারণ:
synchronizedblock বা method-এর ভেতরে ব্লক করা — পর্ব ৪০-এর synchronized-এর monitor lock carrier-এর পরিচয়ের সাথে বাঁধা, তাই Java 21-এর JVM তখন virtual thread-কে সরাতে পারে না।synchronized-এর ভেতরেObject.wait()-ও একই।- native method বা foreign function-এর ভেতর দিয়ে ডাকা — stack-এ C কোডের ফ্রেম থাকলে JVM সেটা heap-এ সরাতে পারে না।
1synchronized (cache) { // monitor নেওয়া2 data = db.query(id); // ব্লকিং I/O3}4lock.lock(); // ReentrantLock5try { data = db.query(id); }6finally { lock.unlock(); }
| carrier C1 | |
| carrier C2 | |
| সারিতে অপেক্ষায় |
- Monitor নেওয়া। virtual thread A carrier C1-এ চলছে, আর
synchronized (cache)-এ ঢুকে monitor নিয়েছে। এখনো সব স্বাভাবিক। - ব্লকিং — কিন্তু নামা যায় না। A ডেটাবেসের উত্তরের অপেক্ষায়। Java 21-এ monitor carrier-এর সাথে বাঁধা, তাই A নামতে পারে না। C1 নামের OS thread-টি পুরো সময় ঘুমিয়ে থাকে — ঠিক পুরোনো platform thread-এর মতো।
- সব carrier আটকে গেলে। carrier মাত্র ২টি। আরেকটি virtual thread X-ও একই ধাঁচের কোডে C2 আটকে দিল। এখন সারিতে হাজারো প্রস্তুত virtual thread, কিন্তু চালানোর মতো একটিও carrier নেই। খারাপ ক্ষেত্রে এমনকি deadlock-এর মতো অবস্থাও হতে পারে।
- ReentrantLock দিয়ে একই কাজ। পর্ব ৪০-এর ReentrantLock
java.util.concurrent-এর lock, যা virtual thread-এর কথা মাথায় রেখে লেখা। - এবার নামা যায়। query-র অপেক্ষায় A নেমে যায় — lock তার হাতেই থাকে, তাই অন্য কেউ ভেতরে ঢুকতে পারে না (নিরাপত্তা একই)। কিন্তু C1 মুক্ত, সে B-কে চালায়।
- ফেরত আর unlock. উত্তর এলে A আবার mount হয়, কাজ শেষ করে
finally-তে lock ছাড়ে। একই সুরক্ষা, কিন্তু carrier একবারও অলস বসে থাকেনি।
সংস্করণ মনে রাখুন। উপরের synchronized-এর সমস্যা Java 21-এর। Java 24-এ JEP 491 (“Synchronize Virtual Threads without Pinning”) JVM-এর monitor-এর নকশা বদলায়, ফলে synchronized-এর ভেতরে ব্লক করলেও virtual thread সাধারণত নেমে যেতে পারে। native ফ্রেমের pinning আর কিছু বিরল ক্ষেত্র এখনো রয়ে গেছে। তাই পরামর্শ: আপনি Java 21-এ থাকলে যে synchronized block-এর ভেতরে I/O হয় আর যা খুব ঘন ঘন চলে, সেগুলো ReentrantLock-এ বদলান। সব synchronized উপড়ে ফেলার দরকার নেই — ছোট, দ্রুত, I/O-হীন synchronized block কোনো সমস্যা করে না। Java 21-এ pinning খুঁজতে -Djdk.tracePinnedThreads=full option আর JFR-এর jdk.VirtualThreadPinned event কাজে লাগে।
Pool করবেন না — সীমা দিন Semaphore দিয়ে
পর্ব ৪১-এ thread pool-এর আকার দুটি কাজ করত: thread-এর খরচ বাঁচানো, আর একসাথে কতগুলো কাজ চলবে তার সীমা। Virtual thread-এ প্রথম কাজটির আর দরকার নেই। দ্বিতীয়টির আছে — ধরুন আপনার ব্যাংক বা মোবাইল অপারেটরের SMS গেটওয়ে বলে দিয়েছে, একসাথে ৫০টির বেশি অনুরোধ পাঠালে তারা প্রত্যাখ্যান করবে। কিন্তু virtual thread-এর একটি “৫০-এর pool” বানানো ভুল পথ। সীমার জন্য সঠিক যন্ত্র হলো Semaphore: কয়েকটি টোকেনের একটি ঝুড়ি — কাজের আগে একটি টোকেন নিন (acquire), শেষে ফেরত দিন (release). টোকেন না থাকলে অপেক্ষা — আর virtual thread-এর অপেক্ষা সস্তা।
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
class Main {
static final Semaphore gateway = new Semaphore(50); // গেটওয়ে একসাথে ৫০টির বেশি নেয় না
static final AtomicInteger inFlight = new AtomicInteger(), peak = new AtomicInteger();
static boolean sendSms(int customer) throws InterruptedException {
gateway.acquire(); // টোকেন না পেলে এখানে অপেক্ষা
try {
peak.accumulateAndGet(inFlight.incrementAndGet(), Math::max);
Thread.sleep(20); // গেটওয়ের উত্তরের অপেক্ষা (ভান)
return customer % 100 != 0; // প্রতি ১০০-তে ১টি নম্বর ভুল (ভান)
} finally {
inFlight.decrementAndGet();
gateway.release(); // টোকেন ফেরত — সবসময়
}
}
public static void main(String[] args) throws Exception {
List<Future<Boolean>> results = new ArrayList<>();
try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
for (int c = 1; c <= 2_000; c++) { int id = c; results.add(pool.submit(() -> sendSms(id))); }
}
long sent = results.stream().filter(Future::resultNow).count();
System.out.println("পাঠানো: " + sent + ", ব্যর্থ: " + (results.size() - sent));
System.out.println("একসাথে ৫০-এর বেশি কখনো? " + (peak.get() > 50));
}
}আউটপুট: পাঠানো: 1980, ব্যর্থ: 20 আর একসাথে ৫০-এর বেশি কখনো? false. ২,০০০ জন গ্রাহকের প্রত্যেকের জন্য একটি করে virtual thread — সবগুলো সাথে সাথে তৈরি হয়। কিন্তু gateway.acquire()-এ মাত্র ৫০টি পার হয়, বাকিরা অপেক্ষা করে। অপেক্ষার সময় তারা carrier থেকে নেমে যায়, তাই কোনো OS thread নষ্ট হয় না। inFlight গোনে এই মুহূর্তে কতজন গেটওয়েতে, আর peak রাখে সর্বোচ্চ — যা কখনো ৫০ ছাড়ায় না। release() রাখা হয়েছে finally-তে, যাতে exception হলেও টোকেন ফেরত যায়; না হলে টোকেন হারাতে হারাতে একসময় সব কাজ থেমে যেত। প্রতি ১০০-তে একটি নম্বর “ভুল” — তাই ২০টি ব্যর্থ। resultNow() (Java 19 থেকে) শেষ হওয়া Future-এর ফল সরাসরি দেয়; executor বন্ধ হওয়ার পর সব কাজ শেষ, তাই এখানে নিরাপদ। সংখ্যাগুলো জমা দেওয়ার ক্রমে গোনা, তাই আউটপুট প্রতিবার এক।
1Semaphore gate = new Semaphore(3);2gate.acquire(); // টোকেন নিন3try { callGateway(); }4finally { gate.release(); } // ফেরত দিন
| খালি টোকেন | |
| গেটওয়েতে | |
| অপেক্ষায় (heap-এ) |
- ঝুড়িতে ৩টি টোকেন। ৫টি virtual thread তৈরি — প্রতিটি কাজের জন্য একটি, pool নয়।
- প্রথম তিনজন ঢোকে। V1, V2, V3 একটি করে টোকেন নেয়, ঝুড়ি খালি। V4 আর V5
acquire()-এ আটকে যায় — কিন্তু virtual thread হওয়ায় carrier থেকে নেমে heap-এ অপেক্ষা করে। - একজন শেষ, একজন ঢোকে। V1-এর কাজ শেষ,
finally-তেrelease(). টোকেনটি সাথে সাথে V4 পায় — গেটওয়েতে আবার ৩ জন। - বাকিরা। V2, V3 শেষ করে দুটি টোকেন ফেরত দেয়; একটি নেয় V5, অন্যটি ঝুড়িতে পড়ে থাকে। একসাথে কখনো ৩-এর বেশি নয়।
- সব শেষ। সবাই টোকেন ফেরত দিয়েছে, ঝুড়িতে আবার ৩টি। সীমা রাখা হলো কাজের সংখ্যা দিয়ে নয়, টোকেনের সংখ্যা দিয়ে — আর thread তৈরি হলো যত খুশি তত।
ThreadLocal — লাখো thread, লাখো কপি
ThreadLocal প্রতিটি thread-কে একটি ভ্যারিয়েবলের নিজস্ব কপি দেয়। পুরোনো দিনে এটা প্রায়ই একটি cache হিসেবে ব্যবহার হতো — যেমন প্রতিটি thread-এর জন্য একটি দামি SimpleDateFormat বা বড় buffer, কারণ pool-এর ২০০টি thread বছরের পর বছর বেঁচে থাকত, আর ২০০টি কপি কোনো ব্যাপার ছিল না। Virtual thread-এ এই হিসাব উল্টে যায়: প্রতিটি কাজের জন্য নতুন thread, তাই cache কখনো পুনর্ব্যবহার হয় না, আর এক লাখ একসাথের কাজ মানে এক লাখ কপি — মেমরি ফুরিয়ে যেতে পারে। Virtual thread ThreadLocal সমর্থন করে (পুরোনো library ভাঙে না), কিন্তু নতুন কোডে এভাবে cache রাখবেন না; দরকার হলে একটি thread-safe ভাগাভাগির object (যেমন immutable DateTimeFormatter) ব্যবহার করুন।
Request-এর তথ্য (কে লগইন করেছে, কোন transaction) এক method থেকে আরেক method-এ না পাঠিয়ে পৌঁছে দেওয়ার জন্য Java 21-এ একটি নতুন API preview হিসেবে এসেছে: ScopedValue (JEP 446). এটি একটি নির্দিষ্ট পরিসরের জন্য অপরিবর্তনীয় মান বাঁধে, আর পরিসর শেষ হলে নিজে থেকেই মুছে যায় — ThreadLocal-এর মতো remove() ভুলে যাওয়ার ঝুঁকি নেই। Java 21-এ এটি preview feature, অর্থাৎ --enable-preview ছাড়া চলে না; পরের সংস্করণগুলোতে কয়েক দফা preview-এর পর Java 25-এ (JEP 506) এটি স্থায়ী হয়েছে। নিজের JDK সংস্করণের documentation দেখে তবেই ব্যবহার করুন।
Structured concurrency — একসাথে শুরু, একসাথে শেষ
একটি request-এর ভেতরে দুটি কাজ একসাথে চালাতে চাইলেন — দাম আর স্টক। একটি ব্যর্থ হলে অন্যটি বাতিল হওয়া উচিত, আর দুটি শেষ না হলে method ফেরত আসা উচিত নয়। এই ধারণার নাম structured concurrency: কাজের জীবনকাল কোডের block-এর সাথে বাঁধা, ঠিক যেমন try block শেষ হলে ভেতরের সব শেষ। Java 21-এ এর API StructuredTaskScope (JEP 453) শুধু preview — নিচের কোডটি ধারণা বোঝানোর জন্য, সাধারণভাবে চলবে না:
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // Java 21-এর preview API
var price = scope.fork(() -> priceApi.fetch(id)); // প্রতিটি fork একটি virtual thread
var stock = scope.fork(() -> stockDb.count(id));
scope.join().throwIfFailed(); // দুটির অপেক্ষা; একটি ব্যর্থ হলে অন্যটি বাতিল
return new Offer(price.get(), stock.get());
}--enable-preview ছাড়া Java 21-এর javac সরাসরি আপত্তি করে:
prog.java:1: error: StructuredTaskScope is a preview API and is disabled by default.
import java.util.concurrent.StructuredTaskScope;
^
(use --enable-preview to enable preview APIs)
পরের সংস্করণগুলোতে এই API-র নকশা বদলেছে (যেমন Java 25-এর preview-তে StructuredTaskScope.open()), তাই উপরের রূপটি মুখস্থ করবেন না — ধারণাটা বুঝুন: “যে block কাজগুলো শুরু করে, সেই block-ই তাদের শেষ দেখে।”
৮ · বাস্তব প্রজেক্টে — framework, thread dump আর migration
বেশিরভাগ মানুষ virtual thread নিজে হাতে তৈরি করবেন না; তাঁদের framework করবে। Spring Boot 3.2 থেকে (Java 21 লাগে) একটি সেটিংই যথেষ্ট — তখন embedded Tomcat প্রতিটি HTTP request একটি virtual thread-এ চালায়, আর Spring-এর আরও কিছু অংশও virtual thread ব্যবহার করে। পর্ব ৫০-এ Spring Boot নিজেই আসবে:
spring.threads.virtual.enabled=trueTomcat আর Jetty-র সাম্প্রতিক সংস্করণ নিজেরাও virtual thread-এ request চালানোর option দেয়; Helidon-এর মতো কিছু framework পুরোপুরি virtual thread ঘিরে নতুন করে লেখা হয়েছে। প্রতিটির নির্দিষ্ট সংস্করণ আর সেটিং তাদের documentation-এ দেখুন — এই এলাকা দ্রুত বদলাচ্ছে।
দেখা ও খোঁজা — thread dump
সমস্যা হলে প্রথম হাতিয়ার thread dump — কোন thread এই মুহূর্তে কোথায় আটকে, তার ছবি। লাখো virtual thread-এর জন্য পুরোনো dump যথেষ্ট নয়, তাই JDK 21-এ jcmd-এর নতুন একটি কমান্ড আছে, যা virtual thread-সহ dump একটি ফাইলে লেখে (সাধারণ লেখা বা JSON):
jcmd <pid> Thread.dump_to_file -format=json threads.jsonযেহেতু virtual thread আসল Thread, ব্যর্থ হলে stack trace-এ পুরো ডাকার পথ দেখা যায় — reactive কোডের তুলনায় এটা বিশাল সুবিধা। ডিবাগারেও ধাপে ধাপে চালানো যায়।
Migration-এর চেকলিস্ট
- JDK 21 বা পরের সংস্করণ — আর আপনার framework ও driver-এর virtual-thread-বান্ধব সংস্করণ।
- কাজটি কি I/O-bound আর একসাথে অনেক? না হলে লাভ সামান্য।
- Thread pool-গুলো খুঁজুন:
newFixedThreadPool(200)-এর মতো যেগুলো শুধু “অনেক অপেক্ষা” সামলাতে, সেগুলোnewVirtualThreadPerTaskExecutor()-এ বদলান। সীমা দরকার হলেSemaphore. - নিচের সম্পদের সীমা মনে রাখুন: ১০,০০০ virtual thread একসাথে ডেটাবেসে গেলে connection pool (ধরুন ২০টি connection) এখন আসল সীমা — সেখানে অপেক্ষাও virtual thread-বান্ধব হতে হবে, আর ডেটাবেস নিজে কতটা চাপ নেবে তা ভাবুন।
- Java 21-এ থাকলে pinning খুঁজুন: ঘন ঘন চলা, ভেতরে I/O থাকা
synchronizedblock →ReentrantLock. JFR বা-Djdk.tracePinnedThreadsদিয়ে যাচাই। - ThreadLocal-এর cache সরান; per-request তথ্যের জন্য ছোট ThreadLocal চলে, তবে নতুন নকশায় ScopedValue-এর দিকে নজর রাখুন।
- মাপুন: আগে-পরে load test — throughput, latency, মেমরি। অনুমানে নয়।
C-র pthread আর C++-এর std::thread সরাসরি OS thread — Java-র platform thread-এর মতোই ভারী; হালকা কাজের জন্য C++20-এ আছে coroutine, কিন্তু সেখানে co_await লিখে কোডকে চিহ্নিত করতে হয়। Python-এর asyncio-ও async/await-নির্ভর: একটি function async হলে তাকে যে ডাকে তাকেও async হতে হয়। Java virtual thread-এর বিশেষত্ব হলো কোনো নতুন keyword নেই — সাধারণ ব্লকিং কোডই হালকা হয়ে যায়, অনেকটা Go-র goroutine-এর মতো। তুলনার জন্য দেখুন C++-এর multithreading আর Python-এর asyncio পর্ব।
- Platform thread আর virtual thread-এর পার্থক্য? — platform thread একটি OS thread-এর মোড়ক, ভারী; virtual thread JVM চালায় carrier thread-এর ওপর, ব্লকিং-এ নেমে যায়, খুব সস্তা।
- Virtual thread কি কোড দ্রুত চালায়? — না; latency একই, একসাথে বেশি কাজ রাখা যায় বলে throughput বাড়ে। CPU-bound কাজে লাভ নেই।
- Virtual thread কি pool করা উচিত? — না; প্রতি কাজে নতুন একটি (
newVirtualThreadPerTaskExecutor). সীমা দরকার হলেSemaphore. - Pinning কী? — virtual thread ব্লক করার সময় carrier থেকে নামতে না পারা; Java 21-এ
synchronized-এর ভেতরে বা native ফ্রেমে হয়। - Virtual thread কোন Java সংস্করণে স্থায়ী? — Java 21, JEP 444 (preview ছিল Java 19 ও 20-এ)।
৯ · AI-এর যুগে — পুরোনো অভ্যাস নতুন thread-এ
AI-এর শেখার উপাদানের বেশিরভাগই Java 21-এর আগের কোড — যেখানে thread মানেই দামি, pool মানেই ভালো অভ্যাস। তাই virtual thread চাইলে AI প্রায়ই পুরোনো অভ্যাস নতুন API-তে বসিয়ে দেয়:
| AI যা করে | কেন সমস্যা | আপনি যা করবেন |
|---|---|---|
newFixedThreadPool(100, Thread.ofVirtual().factory()) — virtual thread-এর pool | pool-এর একমাত্র লাভ (thread বাঁচানো) এখানে নেই; বরং সংখ্যা অকারণে বাঁধা | newVirtualThreadPerTaskExecutor(); সীমা লাগলে Semaphore |
| ছবি resize বা বড় হিসাব virtual thread-এ, “দ্রুত হবে” বলে | CPU-bound কাজে core-এর বেশি গতি অসম্ভব; উল্টো carrier দখল করে I/O কাজ আটকায় | core-এর সমান platform thread-এর pool বা parallel stream |
সব shared কোড synchronized-এ, ভেতরে ডেটাবেস বা HTTP ডাক | Java 21-এ pinning — carrier আটকে throughput ধসে পড়ে | I/O-যুক্ত গরম পথে ReentrantLock; JDK সংস্করণ জেনে সিদ্ধান্ত |
thread-প্রতি বড় ThreadLocal cache (buffer, formatter, connection) | লাখো thread মানে লাখো কপি; cache কখনো পুনর্ব্যবহার হয় না | ভাগাভাগির thread-safe object বা আসল pool (যেমন connection pool) |
Java 21-এ StructuredTaskScope বা ScopedValue-কে স্থায়ী API হিসেবে দেখানো, অথবা পুরোনো preview রূপ | flag ছাড়া compile হয় না; সংস্করণভেদে API আলাদা | preview কি না যাচাই; নিজের JDK-র documentation |
| উদাহরণে thread-এর নাম বা শেষ হওয়ার ক্রম ছাপা, আর সেটাকে “প্রত্যাশিত আউটপুট” বলা | প্রতিবার বদলায়; টেস্ট অনির্ভরযোগ্য | Future জমা দেওয়ার ক্রমে রেখে সমষ্টিগত ফল ছাপা |
Virtual thread-এ কোড লেখাতে বা পুরোনো কোড বদলাতে এই কাঠামো ব্যবহার করুন:
AI-এর virtual thread কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Java-র প্রথম দিকে “green thread” ছিল — JVM-নিয়ন্ত্রিত হালকা thread — যা পরে বাদ দেওয়া হয়। তাহলে virtual thread কি পুরোনো ভুলের পুনরাবৃত্তি?
Green thread কী ছিল। ১৯৯০-এর দশকের মাঝামাঝি, Java 1.0 আর 1.1-এর সময়, বিশেষ করে Sun-এর Solaris-এ, Java thread চলত green thread হিসেবে। নামটি এসেছিল Sun-এর “Green” দলের নাম থেকে, যারা Java-র পূর্বসূরি ভাষা বানিয়েছিল। JVM নিজে thread-গুলোর মধ্যে পালা ঠিক করত, আর সবগুলো চলত একটিমাত্র OS thread-এর ওপর — অর্থাৎ M:1 মডেল। সে সময় অনেক OS-এর নিজস্ব thread-ব্যবস্থা দুর্বল বা অসম্পূর্ণ ছিল, তাই এটাই ছিল বহনযোগ্য সমাধান।
কেন বাদ পড়ল। দুটি মৌলিক দুর্বলতা। প্রথমত, সব thread একটি OS thread-এ থাকায় একাধিক CPU থাকলেও একসাথে একটিই চলত — মাল্টি-প্রসেসর সার্ভার জনপ্রিয় হতেই এটা অগ্রহণযোগ্য হয়ে ওঠে। দ্বিতীয়ত, কোনো thread একটি ব্লকিং system call করলে (যেমন ফাইল পড়া) পুরো OS thread আটকে যেত, আর তার সাথে সব green thread. তাই Java 1.2-1.3-এর সময়ে JVM native OS thread-এ (1:1 মডেল) সরে আসে, আর পরের দুই দশক সেটাই চলে।
Virtual thread কোথায় আলাদা। এক, এটা M:N — অনেক virtual thread চলে core-এর সমান সংখ্যক carrier-এর ওপর, তাই সব CPU ব্যবহার হয়। দুই, JDK-র ব্লকিং কাজগুলো — socket, Thread.sleep, java.util.concurrent-এর lock আর queue — নতুন করে লেখা হয়েছে, যাতে ব্লক করার বদলে virtual thread নেমে যায় আর carrier মুক্ত থাকে। যেখানে তা সম্ভব নয় (কিছু ফাইল কাজ), সেখানে scheduler সাময়িক বাড়তি carrier আনে। তিন, আধুনিক হার্ডওয়্যার আর JIT-এ stack ফ্রেম heap-এ সরানো ও ফেরত আনা অনেক সস্তা। আর চার, OS thread-ও হারিয়ে যায়নি — platform thread পাশাপাশি থাকছে, CPU-ভারী কাজের জন্য।
অর্থাৎ green thread-এর ব্যর্থতা থেকে শিক্ষা নেওয়া হয়েছে: সমস্যা হালকা thread-এর ধারণায় ছিল না, ছিল M:1 মডেলে আর ব্লকিং I/O-কে সঠিকভাবে না সামলানোয়। ২৫ বছর পরে JDK-কে নিচ থেকে ওপর পর্যন্ত প্রস্তুত করে তবেই ধারণাটি ফিরে এসেছে।
মূল উপলব্ধি: virtual thread পুরোনো green thread নয়, তার সংশোধিত রূপ — M:1-এর জায়গায় M:N, আর ব্লকিং-এ পুরো thread আটকানোর বদলে নেমে যাওয়া; ধারণা একই, প্রকৌশল সম্পূর্ণ আলাদা।
প্র ০২Project Loom শুরু হয়েছিল ২০১৭ সালের দিকে, অথচ virtual thread স্থায়ী হলো ২০২৩-এ। এত সময় লাগল কেন?
শুরু। Project Loom OpenJDK-র একটি প্রকল্প, যার নেতৃত্বে ছিলেন Ron Pressler, সাথে Alan Bateman-সহ Oracle-এর Java প্ল্যাটফর্ম দল। Pressler আগে Quasar নামে একটি library বানিয়েছিলেন, যা bytecode বদলে Java-তে হালকা “fiber” দিত। সেই অভিজ্ঞতা থেকে তিনি বুঝেছিলেন, library দিয়ে এটা পুরোপুরি ঠিকভাবে হয় না — JVM আর JDK-র ভেতরেই কাজ করতে হবে। Loom প্রস্তাব করা হয় ২০১৭ সালের দিকে। নামটি তাঁতের (loom) — thread আর fiber বোনার যন্ত্র।
পথে কী কী বদলাল। শুরুতে পরিকল্পনা ছিল আলাদা একটি Fiber class. কিন্তু তাতে পুরো Java জগৎকে দুই ভাগ হতে হতো — thread-এর জন্য লেখা library fiber-এ চলত না। দল সিদ্ধান্ত নেয়: নতুন জিনিসটি হবে java.lang.Thread-ই, যাতে পুরোনো কোড, debugger, profiler, stack trace সব চলে। এই সিদ্ধান্তের মানে ছিল JDK-র বিশাল অংশ নতুন করে লেখা: socket-এর পুরোনো বাস্তবায়ন বদলানো হয় (Java 13-এ JEP 353, Java 15-এ JEP 373 — দুটিই Loom-এর প্রস্তুতি), java.util.concurrent-এর lock-গুলো virtual-thread-বান্ধব করা হয়, thread dump, JFR আর debugger interface-এ সমর্থন যোগ হয়।
Preview থেকে স্থায়ী। Java 19 (সেপ্টেম্বর ২০২২)-এ JEP 425 দিয়ে প্রথম preview, Java 20-এ JEP 436 দিয়ে দ্বিতীয়, আর Java 21 (সেপ্টেম্বর ২০২৩)-এ JEP 444 দিয়ে স্থায়ী। Preview-এর সময় পাওয়া মতামতে কিছু জিনিস বদলায় — যেমন শুরুতে virtual thread-এ ThreadLocal বন্ধ রাখার একটি option ছিল, স্থায়ী রূপে virtual thread সবসময় ThreadLocal সমর্থন করে, কারণ অসংখ্য পুরোনো library এর ওপর নির্ভর করে। Structured concurrency আর scoped value আলাদা JEP-এ ভাগ হয়ে নিজেদের গতিতে এগোয়। Java 21 একটি LTS সংস্করণ হওয়ায় বড় প্রতিষ্ঠানগুলো এটি নির্ভয়ে গ্রহণ করতে পারে।
মূল উপলব্ধি: virtual thread-এর আসল কাজ ছিল একটি নতুন class নয়, পুরো JDK-কে নিচ থেকে প্রস্তুত করা যাতে নতুন জিনিসটি পুরোনো Thread-ই থাকে — সামঞ্জস্য রাখার এই সিদ্ধান্তই ছয় বছরের কারণ, আর এটাই virtual thread-কে সহজে গ্রহণযোগ্য করেছে।
প্র ০৩Go-র goroutine আর Erlang-এর process অনেক আগে থেকেই লাখো হালকা কাজ চালায়। Java তাদের কাছ থেকে কী নিল, আর কোথায় ভিন্ন পথে গেল?
Erlang. ১৯৮০-এর দশকে Ericsson-এ Joe Armstrong ও তাঁর সহকর্মীরা টেলিফোন সুইচের জন্য Erlang বানান। একটি সুইচে একসাথে হাজারো কল, আর প্রতিটি কল একটি স্বাধীন কাজ। Erlang-এর “process” OS process নয় — রানটাইম (পরে BEAM ভার্চুয়াল মেশিন) নিজে চালায়, প্রতিটি খুব ছোট, আর এদের মধ্যে কোনো মেমরি ভাগাভাগি নেই; কথা হয় শুধু message পাঠিয়ে। Scheduler প্রতিটি process-কে নির্দিষ্ট পরিমাণ কাজের পর থামিয়ে অন্যকে সুযোগ দেয়, তাই একটি ভারী process বাকিদের আটকাতে পারে না।
Go. ২০০৯ সালে Google-এ প্রকাশিত Go ভাষার goroutine একটি ছোট stack নিয়ে শুরু হয় আর দরকার মতো বাড়ে; Go রানটাইম লাখো goroutine-কে অল্প কয়েকটি OS thread-এ M:N পদ্ধতিতে চালায়। সবচেয়ে বড় কথা, Go-তে কোনো async/await নেই — সাধারণ ব্লকিং কোড লিখলেই রানটাইম পেছনে সব সামলায়। এই অভিজ্ঞতা দেখিয়েছিল যে “সহজ ব্লকিং কোড + বিশাল scale” বাস্তবে সম্ভব।
Java কী নিল। Go-র মূল শিক্ষা: নতুন রঙের function (async) না বানিয়ে পুরোনো ব্লকিং মডেলটিকেই সস্তা করা। ছোট, বাড়তে-পারা stack আর M:N scheduling-ও একই নীতি। Erlang থেকে এসেছে “প্রতিটি কাজের জন্য একটি নিজস্ব হালকা ধারা” ভাবনার সংস্কৃতি।
কোথায় ভিন্ন। (১) Java-কে তিন দশকের পুরোনো কোডের সাথে মিলতে হয়, তাই virtual thread একটি নতুন ধারণা নয়, পুরোনো Thread class-এরই আরেক রূপ। (২) Erlang-এর মতো মেমরি-বিচ্ছিন্নতা নেই — Java-র virtual thread-ও shared memory দেখে, তাই পর্ব ৪০-এর race condition আর lock-এর সব নিয়ম এখনো খাটে। (৩) Java 21-এর scheduler সময় ধরে জোর করে থামায় না; একটি CPU-ভারী virtual thread carrier ধরে রাখতে পারে — Erlang আর আধুনিক Go দুটোই এ ক্ষেত্রে কোনো না কোনোভাবে জোর করে পালা বদলায়। (৪) Java এখনো platform thread রাখছে, Go-তে শুধু goroutine.
মূল উপলব্ধি: Java হালকা thread-এর ধারণা নিয়েছে Go আর Erlang-এর প্রমাণিত অভিজ্ঞতা থেকে, কিন্তু সামঞ্জস্যের দায় থেকে নিজের পথ বেছেছে — নতুন ভাষা নয়, পুরোনো Thread-কেই সস্তা করা, shared memory আর তার সব সতর্কতা সহ।
প্র ০৪Little's law দিয়ে ব্যাখ্যা করুন: শুধু “অপেক্ষা সস্তা করা” কীভাবে একটি সার্ভারের throughput বাড়ায়, আর কোথায় গিয়ে এই লাভ থেমে যায়?
সূত্র। John D. C. Little ১৯৬১ সালে এর সাধারণ প্রমাণ দেন: একটি স্থিতিশীল সিস্টেমে L = λ × W, যেখানে L গড়ে কতগুলো কাজ একসাথে ভেতরে আছে, λ প্রতি সেকেন্ডে কতগুলো কাজ আসে-যায় (throughput), আর W প্রতিটি কাজ গড়ে কত সময় থাকে (latency). সূত্রটি সিস্টেমের ভেতরের খুঁটিনাটির ওপর নির্ভর করে না — ব্যাংকের লাইন, টোল প্লাজা, ওয়েব সার্ভার সবখানে খাটে।
সার্ভারে প্রয়োগ। λ = L ÷ W. ধরুন প্রতিটি request-এর W = ২০০ মিলিসেকেন্ড, যার বেশিরভাগ ডেটাবেস আর অন্য সার্ভিসের অপেক্ষা। W কমানো কঠিন — ডেটাবেসকে তো দ্রুত করা যাচ্ছে না। কাজেই throughput বাড়ানোর একমাত্র পথ L বাড়ানো, অর্থাৎ একসাথে বেশি request ধরে রাখা। Thread-per-request-এ L = thread-এর সংখ্যা। ২০০টি platform thread-এ λ সর্বোচ্চ ২০০ ÷ ০.২ = ১,০০০ request প্রতি সেকেন্ড, CPU যতই অলস থাকুক। Virtual thread L-কে ২০০ থেকে ১০,০০০-এ নিতে দেয়, তাই তাত্ত্বিক সীমা ৫০,০০০ request প্রতি সেকেন্ড। এ জন্যই JEP 444-এর লেখকেরা Little's law দিয়েই virtual thread-এর যুক্তি দিয়েছেন।
লাভ কোথায় থামে। L বাড়ালে throughput বাড়ে যতক্ষণ অন্য কোনো সম্পদ সীমায় না পৌঁছায়। (১) CPU: প্রতিটি request-এ ২ মিলিসেকেন্ড CPU লাগলে ৮টি core সর্বোচ্চ প্রায় ৪,০০০ request প্রতি সেকেন্ড সামলাবে — তারপর L বাড়ালে শুধু W বাড়ে। (২) নিচের সম্পদ: ডেটাবেসের connection pool-এ ২০টি connection থাকলে ডেটাবেসের দিক থেকে L আসলে ২০; বাকিরা connection-এর লাইনে দাঁড়ায়। (৩) বাইরের সার্ভিস: সেটা যদি একসাথে ৫০টির বেশি না নেয়, তাহলে সেখানে L = ৫০ — এ জন্যই Semaphore. (৪) মেমরি: প্রতিটি request-এর নিজের ডেটা (JSON, ফলাফল) লাখো গুণ হলে heap ফুরায়।
অর্থাৎ virtual thread একটি নির্দিষ্ট বাধা সরায় — “thread-এর সংখ্যা” — আর তার পরে যে বাধা আছে, সেটাই নতুন সীমা হয়ে ওঠে। ভালো প্রকৌশলী আগে মাপেন, কোন বাধাটি সত্যিই সীমা।
মূল উপলব্ধি: latency একই থাকলে throughput বাড়ে কেবল একসাথের কাজ বাড়িয়ে; virtual thread সেই সংখ্যাকে সস্তা করে, কিন্তু CPU, ডেটাবেস আর বাইরের সার্ভিসের সীমা বদলায় না — তাই লাভ ততক্ষণই, যতক্ষণ thread-ই ছিল আসল বাধা।
প্র ০৫Virtual thread আসার পর reactive framework (WebFlux, RxJava, Vert.x) কি হারিয়ে যাবে? একটি ভারসাম্যপূর্ণ উত্তর দিন।
যে কারণে reactive এসেছিল। ২০১০-এর দশকে reactive framework জনপ্রিয় হয় মূলত একটি কারণে: platform thread দামি, তাই লাখো সংযোগ সামলাতে thread-কে অপেক্ষায় বসিয়ে রাখা চলবে না। এই একটি কারণের জন্য প্রোগ্রামাররা callback-এর শিকল, কঠিন ডিবাগিং আর পুরো কোডবেস async করার দাম দিয়েছিলেন (অংশ ২-এর টেবিল)। Virtual thread ঠিক এই কারণটিকে সরিয়ে দেয়। তাই যেসব প্রজেক্ট শুধু scale-এর জন্য reactive বেছেছিল, তাদের অনেকেই নতুন কোডে সাধারণ ব্লকিং ধাঁচে ফিরতে পারে — Spring MVC + virtual thread অনেক ক্ষেত্রে WebFlux-এর কাছাকাছি scale দেয়, অনেক সহজ কোডে।
যেখানে reactive এখনো শক্তিশালী। (১) Stream-আকারের ডেটা: একটানা আসা ঘটনা (stock-এর দাম, sensor, chat message) যেখানে back-pressure — গ্রাহক ধীর হলে উৎসকে ধীর করা — একটি মূল চাহিদা; reactive library-তে এটা প্রথম শ্রেণির ধারণা। (২) জটিল সময়-নির্ভর মিশ্রণ: কয়েকটি ঘটনার ধারা মিলিয়ে, সময়সীমা দিয়ে, debounce করে — operator-এর ভাষায় এগুলো সংক্ষেপে লেখা যায়। (৩) বিদ্যমান বিশাল কোডবেস আর দক্ষ দল — চালু, পরীক্ষিত সিস্টেম নতুন করে লেখার খরচ বড়। (৪) Netty-র মতো নিচের স্তরের নেটওয়ার্ক library, যা নিজেই event loop-ভিত্তিক আর অত্যন্ত অপ্টিমাইজড।
সম্ভাব্য ভবিষ্যৎ। ইতিহাস বলে প্রযুক্তি সাধারণত হঠাৎ হারায় না, নিজের জায়গায় সরে যায়। সম্ভবত সাধারণ request-response ব্যবসায়িক সার্ভিসের ডিফল্ট হয়ে উঠবে ব্লকিং কোড + virtual thread, আর reactive থেকে যাবে সত্যিকারের streaming আর event-প্রক্রিয়ার কাজে। অনেক reactive library নিজেও virtual thread-এর সাথে মিলে চলার পথ খুঁজছে। Structured concurrency স্থায়ী হলে ব্লকিং ধাঁচে একসাথে কয়েকটি কাজ চালানো আরও পরিষ্কার হবে, যা reactive-এর আরেকটি সুবিধার জায়গা দখল করবে।
মূল উপলব্ধি: reactive-এর যে অংশ শুধু “thread দামি” সমস্যার সমাধান ছিল, virtual thread তার প্রয়োজন অনেকটাই কমিয়ে দেয়; কিন্তু back-pressure আর ঘটনার ধারা মেলানোর মতো আসল streaming সমস্যায় reactive এখনো সঠিক হাতিয়ার — বাছাই হবে সমস্যার আকার দেখে, ফ্যাশন দেখে নয়।
অনুশীলন
বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — কাজটি অপেক্ষার না হিসাবের, আর সীমা কোথায়, তা নিয়ে ভাবাই আসল কাজ।
-
হিসাব করুন: একটি সার্ভারে প্রতিটি request গড়ে ৫০০ মিলিসেকেন্ড নেয় (প্রায় পুরোটাই অন্য সার্ভিসের অপেক্ষা)। (ক) ১০০টি platform thread-এর pool হলে সর্বোচ্চ কত request প্রতি সেকেন্ডে? (খ) প্রতি সেকেন্ডে ৫,০০০ request সামলাতে একসাথে কতগুলো request চলতে হবে?
Little's law: λ = L ÷ W. (ক) λ = ১০০ ÷ ০.৫ = ২০০ request প্রতি সেকেন্ডে — CPU প্রায় অলস থাকলেও এর বেশি নয়। (খ) L = λ × W = ৫,০০০ × ০.৫ = ২,৫০০টি একসাথের request. ২,৫০০টি platform thread অনেক সার্ভারে কষ্টকর, কিন্তু ২,৫০০টি virtual thread তুচ্ছ। তবে মনে রাখুন, যে সার্ভিসের অপেক্ষা হচ্ছে, তাকেও একসাথে ২,৫০০টি অনুরোধ নিতে পারতে হবে — নইলে সীমা সেখানে।
-
বাছাই করুন: প্রতিটির জন্য বলুন virtual thread লাভ দেবে কি না, আর কেন। (ক) ২০,০০০ ছবির thumbnail বানানো। (খ) ৫০,০০০ গ্রাহকের ঠিকানা একটি geocoding API দিয়ে যাচাই। (গ) একটি chat সার্ভার, যেখানে ১ লাখ ব্যবহারকারী সংযুক্ত, কিন্তু বেশিরভাগ চুপ। (ঘ) একটি command-line টুল, যা ৩টি ফাইল পরপর পড়ে।
(ক) না — CPU-bound; core-এর সমান platform thread-এর pool বা parallel stream. (খ) হ্যাঁ — প্রতিটি কাজ মূলত নেটওয়ার্কের অপেক্ষা; সাথে API-র rate limit অনুযায়ী Semaphore. (গ) হ্যাঁ — প্রতিটি সংযোগ বেশিরভাগ সময় পরের message-এর অপেক্ষা; প্রতি সংযোগে একটি virtual thread একটি স্বাভাবিক নকশা (তবে প্রতি সংযোগের মেমরি, যেমন buffer, হিসাবে রাখুন)। (ঘ) না — মাত্র ৩টি কাজ, পরপর; virtual thread দিলে শুধু জটিলতা বাড়ে।
-
Bug খুঁজুন: একজন সহকর্মী Java 21-এ লিখেছেন:
synchronized String getToken() { if (expired()) token = authServer.refresh(); return token; }— প্রতিটি request এটা ডাকে, আরrefresh()একটি HTTP ডাক যা মাঝে মাঝে ২ সেকেন্ড নেয়। virtual thread চালু করার পর load test-এ মাঝে মাঝে পুরো সার্ভার কয়েক সেকেন্ড থমকে যায়। কেন, আর সমাধান কী?Pinning.
refresh()চলার সময় যে virtual thread lock ধরে আছে, সে Java 21-এsynchronized-এর ভেতরে ব্লক করছে, তাই তার carrier আটকে যায়। বাকি request-গুলোgetToken()-এ ঢুকতে গিয়ে monitor-এর জন্য অপেক্ষা করে — আর Java 21-এ monitor-এ ঢোকার অপেক্ষাতেও virtual thread pinned থাকে। অল্প সময়েই সব carrier এমন thread-এ ভরে যায়, অন্য কোনো কাজ (যেগুলোর token দরকারই নেই) চলতে পারে না — সার্ভার থমকে যায়। সমাধান:ReentrantLockদিয়ে একই যুক্তি (lock.lock(); try { … } finally { lock.unlock(); }), যাতে অপেক্ষার সময় virtual thread নেমে যায়; আরও ভালো, token মেয়াদ শেষ হওয়ার আগেই একটি আলাদা কাজে রিফ্রেশ করা, যাতে request-এর পথে HTTP ডাকই না থাকে। Java 24 বা পরের সংস্করণে JEP 491-এর পর এই pinning সাধারণত হয় না, কিন্তু একটি lock-এর পেছনে সব request-এর ২ সেকেন্ড দাঁড়িয়ে থাকার নকশাগত সমস্যা থেকেই যায়। -
কোড বদলান (একমাত্র কোড লেখার কাজ): পুরোনো কোডটি
ExecutorService pool = Executors.newFixedThreadPool(30);দিয়ে প্রতি রাতে ৮,০০০টি ইনভয়েস একটি পেমেন্ট API-তে পাঠায়; ৩০ রাখা হয়েছিল কারণ API একসাথে ৩০টির বেশি অনুরোধ নেয় না। virtual thread-এ বদলে লিখুন, সীমা বজায় রেখে।Semaphore paymentApi = new Semaphore(30); // সীমা এখন এখানে List<Future<Boolean>> results = new ArrayList<>(); try (var pool = Executors.newVirtualThreadPerTaskExecutor()) { for (Invoice inv : invoices) { results.add(pool.submit(() -> { paymentApi.acquire(); try { return api.send(inv); } finally { paymentApi.release(); } })); } }pool-এর আকার যে দায়িত্ব পালন করত (একসাথে ৩০টি), সেটা এখন স্পষ্টভাবে Semaphore-এর নামে লেখা — কোড পড়লেই বোঝা যায় সীমাটা কেন। ৮,০০০টি virtual thread সাথে সাথে তৈরি হয়, কিন্তু ৩০টির বেশি একসাথে API-তে যায় না; বাকিরা সস্তায় অপেক্ষা করে।
release()অবশ্যইfinally-তে। এই নির্দিষ্ট কাজে মোট সময় খুব একটা বদলাবে না (সীমা তো API-র), কিন্তু একই প্রোগ্রামে অন্য কাজগুলো আর ৩০-এর pool-এর জন্য লাইনে দাঁড়াবে না।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমJava 21-এ একটি virtual thread ডেটাবেসের উত্তরের অপেক্ষা করলে কী কী ঘটে — ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📋 virtual thread তৈরি হয়ে scheduler-এর সারিতে ঢোকে
- ⚙️ একটি carrier thread-এ mount হয়ে তার কোড চলে
- ⏳ socket থেকে পড়তে গিয়ে উত্তর না পেয়ে ব্লক হয়
- 🗄️ তার stack heap-এ সরে যায় (unmount), carrier অন্য virtual thread নেয়
- 🔔 উত্তর এলে আবার সারিতে ঢোকে, পরে কোনো carrier-এ mount হয়ে পরের লাইন থেকে চলে
Java 21-এ একটি virtual thread ব্লকিং I/O-র অপেক্ষায় থাকলে সাধারণত কী ঘটে?
- যে OS thread-এ চলছিল, সেটি পুরো সময় ঘুমিয়ে থাকে
- carrier থেকে নেমে যায়, stack heap-এ থাকে, carrier অন্য কাজ নেয়
- একটি exception ছোড়ে, কারণ virtual thread-এ ব্লক করা নিষেধ
- JVM সেই কাজের জন্য একটি নতুন platform thread তৈরি করে
৮ core-এর একটি মেশিনে একটি CPU-ভারী হিসাব (কোনো I/O নেই) ১০,০০০ ভাগে ভাগ করে ১০,০০০টি virtual thread-এ চালালে, ৮টি platform thread-এর তুলনায় কী আশা করা যায়?
- প্রায় ১,২৫০ গুণ দ্রুত
- প্রায় ৮ গুণ দ্রুত
- compile বা runtime error
- মোটামুটি একই সময়, বাড়তি গতি নেই
একটি বাইরের API একসাথে সর্বোচ্চ ৫০টি অনুরোধ নেয়। virtual thread ব্যবহার করে এই সীমা রাখার সুপারিশকৃত উপায় কী?
- newVirtualThreadPerTaskExecutor() আর Semaphore(50)
- ৫০টি virtual thread-এর একটি fixed pool
- API ডাকটি একটি synchronized method-এ রাখা
- virtual thread-এর priority কমিয়ে দেওয়া
Java 21-এ virtual thread-এর “pinning” বলতে কী বোঝায়?
- virtual thread একটি নির্দিষ্ট CPU core-এ স্থায়ীভাবে বাঁধা থাকা
- virtual thread-এর মেমরি GC ফেরত নিতে না পারা
- ব্লক করার সময় virtual thread carrier থেকে নামতে না পারা, ফলে carrier-ও আটকে থাকা
- নাম ছাড়া virtual thread তৈরি করা
Java-তে virtual thread কোন সংস্করণে এবং কোন JEP-এর মাধ্যমে স্থায়ী (final) বৈশিষ্ট্য হয়?
- Java 19, JEP 425
- Java 21, JEP 444
- Java 17, JEP 444
- Java 24, JEP 491
Java-তে লাখো virtual thread-এর সাথে ThreadLocal দিয়ে প্রতি thread-এ একটি বড় cache (যেমন ১ MB buffer) রাখলে মূল সমস্যা কী?
- virtual thread-এ ThreadLocal ব্যবহার করলে compile error হয়
- সব virtual thread একই buffer ভাগ করে, তাই ডেটা মিশে যায়
- ThreadLocal পড়লেই carrier pinned হয়
- প্রতিটি thread-এর আলাদা কপি — লাখো কপি, মেমরি ফুরানোর ঝুঁকি, আর cache পুনর্ব্যবহারও হয় না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- সমস্যা thread-এর দাম: platform thread = OS thread, ভারী; thread-per-request-এ সার্ভার thread ফুরিয়ে আটকে যায়, যদিও বেশিরভাগ thread শুধু অপেক্ষা করে। Little's law: throughput = একসাথের কাজ ÷ latency.
- Virtual thread = সস্তা অপেক্ষা: Java 21 (JEP 444)-এ স্থায়ী; carrier-এ mount হয়ে চলে, ব্লকিং-এ নেমে যায়, stack থাকে heap-এ। কোড দ্রুত নয় — একসাথে অনেক বেশি কাজ।
- তৈরি:
Thread.ofVirtual().start,Thread.startVirtualThread, আর সবচেয়ে কাজেরExecutors.newVirtualThreadPerTaskExecutor()try-with-resources-এ;join,interrupt,isVirtualসব আছে। - নিয়ম: pool নয়, প্রতি কাজে নতুন; সীমা Semaphore দিয়ে; CPU-bound কাজে লাভ নেই; বড় ThreadLocal cache নয়।
- সংস্করণ মেনে: Java 21-এ
synchronized-এর ভেতরে ব্লকিং carrier আটকায় (pinning) — গরম পথেReentrantLock; Java 24-এর JEP 491 এর বেশিরভাগ দূর করেছে। ScopedValue আর StructuredTaskScope Java 21-এ preview.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৩ · নেটওয়ার্কিং — socket, HttpClient আর WebSocketপরবর্তী পর্বTCP socket, HttpClient আর virtual thread দিয়ে সার্ভার।
- Java প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Python · asyncio — async/awaitতুলনাএকই সমস্যার async সমাধান — আর তার “রঙিন function”-এর দাম।
- C++ · multithreadingপাশের ট্র্যাকOS thread সরাসরি — platform thread-এর নিচে কী আছে।
- অপারেটিং সিস্টেমগভীরে যানprocess, thread, scheduling আর context switch-এর মূল ধারণা।