Aplikasi server klasik Java lama terjepit dua pilihan: “satu thread per permintaan” yang mentok di ribuan klien karena thread platform mahal, atau pool kecil dengan gaya asinkron non-blocking yang sulit dibaca dan dilacak. Virtual threads, difinalkan di Java 21 melalui JEP 444, keluar dari jepitan itu: thread yang dikelola JVM dengan tumpukan hemat memori di heap, sehingga jutaan penunggu I/O menjadi wajar. Chapter ini merangkum konsep intinya dari sisi bahasa modern — pembuatan lewat builder dan executor, aturan pakai yang menjaga manfaatnya, serta batas kapan thread platform tetap tepat. Pembahasan mendalam tentang penjadwalan dan diagnosa sudah tersedia di Section Concurrency dan Multithreading.
Thread Ringan yang Dikelola JVM #
Sebuah virtual thread tidak berkorespondensi dengan thread sistem operasi. Eksekusinya berlangsung dengan menempel pada carrier thread (thread platform yang dikelola ForkJoinPool khusus), sementara tumpukannya disimpan di heap sebagai segmen objek yang tumbuh dan menyusut sesuai kebutuhan. Saat virtual thread memanggil operasi pemblokir — Thread.sleep, baca socket, lock() — JVM melepasnya dari carrier, memarkir keadaannya, dan memakai carrier untuk menjalankan virtual thread lain. Menunggu menjadi nyaris gratis; kode tetap berbentuk berurutan yang mudah dibaca, tanpa callback dan tanpa rantai reaktif.
Dua Cara Membuat Virtual Thread #
Contoh teruji berikut memakai bentuk builder untuk thread bernama dan bentuk executor untuk kipas (fan-out) tiga layanan:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class KipasLayanan {
// Meniru panggilan layanan luar: hampir seluruh waktunya hanya menunggu.
static String muat(String layanan, int milidetik) throws InterruptedException {
Thread.sleep(milidetik);
return layanan + " selesai (" + milidetik + " ms)";
}
public static void main(String[] args) throws Exception {
// 1) Bentuk langsung: virtual thread bernama lewat builder.
String[] nama = {"katalog", "persediaan", "ulasan"};
Thread[] pekerja = new Thread[3];
String[] hasilLangsung = new String[3];
for (int i = 0; i < 3; i++) {
int idx = i;
pekerja[i] = Thread.ofVirtual().name("kurator-" + nama[i]).start(() ->
hasilLangsung[idx] = nama[idx] + ": data termuat");
}
for (Thread t : pekerja) {
t.join();
}
for (int i = 0; i < 3; i++) {
System.out.println(pekerja[i].getName() + " | virtual: "
+ pekerja[i].isVirtual() + " | " + hasilLangsung[i]);
}
// 2) Bentuk executor: satu virtual thread per task.
try (ExecutorService eksekutor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = new ArrayList<>();
futures.add(eksekutor.submit(() -> muat("katalog", 40)));
futures.add(eksekutor.submit(() -> muat("persediaan", 20)));
futures.add(eksekutor.submit(() -> muat("ulasan", 60)));
List<String> hasil = new ArrayList<>();
for (Future<String> f : futures) {
hasil.add(f.get());
}
hasil.sort(null); // urutan cetak tetap deterministik
hasil.forEach(System.out::println);
} // close(): menunggu seluruh task tuntas
}
}Output yang Dihasilkan:
kurator-katalog | virtual: true | katalog: data termuat
kurator-persediaan | virtual: true | persediaan: data termuat
kurator-ulasan | virtual: true | ulasan: data termuat
katalog selesai (40 ms)
persediaan selesai (20 ms)
ulasan selesai (60 ms)Bedah baris-per-baris untuk bagian pentingnya:
- Baris
Thread.ofVirtual().name("kurator-" + nama[i]).start(...): builder mengembalikan virtual thread yang diberi nama eksplisit — nama ini tampil pada thread dump sehingga penelusuran tetap mudah meski jumlah thread jutaan. - Baris
pekerja[i].isVirtual(): seluruh thread buatan builder bernilaitrue; APIThreadlama —join(),interrupt(),isAlive()— bekerja identik. - Baris
Executors.newVirtualThreadPerTaskExecutor(): setiapsubmitmendapat virtual thread segar; tidak ada ukuran pool yang perlu dihitung karena batas berasal dari sumber daya hulu seperti koneksi basis data. - Blok
hasil.sort(null)dan cetak setelah semuaget(): hasil antar thread dikumpulkan dulu, baru dicetak terurut — pola wajib agar keluaran program konkuren tetap deterministik.
Aturan Pakai dan Batasnya #
| Aspek | Thread platform | Virtual thread |
|---|---|---|
| Dikelola oleh | Sistem operasi | JVM (carrier ForkJoinPool) |
| Tumpukan per thread | ±1 MB, tetap sejak dibuat | Kecil, di heap, tumbuh dan menyusut |
| Jumlah wajar | Ribuan | Jutaan |
| Cocok untuk | Komputasi CPU padat | Menunggu I/O |
| Pool thread | Lazim | Jangan — satu thread per task |
Kekhawatiran lama soal pinning (virtual thread terkatup pada carrier saat synchronized memblokir) sudah dibenahi sejak Java 24 melalui JEP 491; yang tersisa terutama panggilan native yang memblokir dari dalam. Untuk komputasi murni berdurasi panjang, virtual thread tidak memberi keuntungan — tidak ada penungguan yang bisa diparkir — sehingga ForkJoinPool atau parallel stream tetap pilihan yang tepat.
Jutaan thread yang hidup bebas menimbulkan pertanyaan tata kelola baru: siapa yang menunggu dan membatalkan mereka sebagai satu unit? Structured concurrency menjawabnya, dan ScopedValue menggantikan ThreadLocal sebagai jalur berbagi data — keduanya dibahas di chapter-chapter akhir section ini. Sebelum sampai di sana, lapisan koleksi lebih dulu mendapat penyeragaman API lewat sequenced collections.
Referensi #
- OpenJDK: JEP 444 — Virtual Threads
- Oracle: Class Thread (Java API Specification)
- OpenJDK: JEP 491 — Synchronize Virtual Threads without Pinning
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
KipasLayanan.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class KipasLayanan {
// Meniru panggilan layanan luar: hampir seluruh waktunya hanya menunggu.
static String muat(String layanan, int milidetik) throws InterruptedException {
Thread.sleep(milidetik);
return layanan + " selesai (" + milidetik + " ms)";
}
public static void main(String[] args) throws Exception {
// 1) Bentuk langsung: virtual thread bernama lewat builder.
String[] nama = {"katalog", "persediaan", "ulasan"};
Thread[] pekerja = new Thread[3];
String[] hasilLangsung = new String[3];
for (int i = 0; i < 3; i++) {
int idx = i;
pekerja[i] = Thread.ofVirtual().name("kurator-" + nama[i]).start(() ->
hasilLangsung[idx] = nama[idx] + ": data termuat");
}
for (Thread t : pekerja) {
t.join();
}
for (int i = 0; i < 3; i++) {
System.out.println(pekerja[i].getName() + " | virtual: "
+ pekerja[i].isVirtual() + " | " + hasilLangsung[i]);
}
// 2) Bentuk executor: satu virtual thread per task.
try (ExecutorService eksekutor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = new ArrayList<>();
futures.add(eksekutor.submit(() -> muat("katalog", 40)));
futures.add(eksekutor.submit(() -> muat("persediaan", 20)));
futures.add(eksekutor.submit(() -> muat("ulasan", 60)));
List<String> hasil = new ArrayList<>();
for (Future<String> f : futures) {
hasil.add(f.get());
}
hasil.sort(null); // urutan cetak tetap deterministik
hasil.forEach(System.out::println);
} // close(): menunggu seluruh task tuntas
}
}