Virtual thread adalah jenis thread yang dijadwalkan langsung oleh JVM, bukan oleh sistem operasi, sehingga biaya pembuatannya mendekati biaya membuat objek biasa. Java 21 menjadikannya fitur final, dan kehadirannya mengubah cara program Java menangani ribuan tugas sekaligus. Topik ini membahas persoalan yang diselesaikan virtual thread, cara membuatnya dengan Thread.ofVirtual() dan Executors.newVirtualThreadPerTaskExecutor(), serta demo yang meluncurkan seribu tugas konkuren sekaligus.
Biaya platform thread#
Thread yang dikenal sebelum Java 21 kini disebut platform thread, yaitu thread Java yang dipetakan satu banding satu ke thread milik sistem operasi. Setiap platform thread menuntut memori untuk tumpukannya, lazimnya berukuran satu megabyte secara bawaan, dan pembuatannya melibatkan panggilan sistem yang tidak murah. Jumlah platform thread yang sehat pada satu program pun terbatas, lazimnya bergerak di kisaran ribuan, bukan puluhan ribu. Batas ini jarang terasa pada program kecil, tetapi langsung terlihat pada server yang menangani banyak permintaan bersamaan.
Persoalan utamanya adalah menunggu. Tugas yang membaca berkas, menunggu respons jaringan, atau mengambil data dari database menghabiskan sebagian besar hidupnya dalam keadaan blocking, yakni menahan thread sembari tidak mengerjakan apa pun. Selama menunggu, platform thread yang memegang tugas itu tidak dapat dipakai untuk pekerjaan lain, sehingga server dengan beban I/O yang padat membutuhkan banyak thread hanya untuk menunggu. Virtual thread menjawab persoalan ini dengan membuat biaya menunggu menjadi murah, karena thread yang tertahan tidak lagi menyandera sumber daya mahal.
Membuat virtual thread pertama#
Cara paling langsung membuat virtual thread adalah Thread.ofVirtual().start(...), yaitu pabrik yang menerima Runnable dan langsung menjalankannya. Program berikut meluncurkan satu tugas virtual lalu menunggunya selesai dengan join().
public class DemoVirtualPertama {
public static void main(String[] args) throws InterruptedException {
Thread t = Thread.ofVirtual().start(() -> {
System.out.println("Halo dari thread virtual");
});
t.join();
}
}Halo dari thread virtualMenjalankannya dari terminal memakai perintah yang sama dengan program Java biasa.
# berada di folder tempat DemoVirtualPertama.java disimpan
java DemoVirtualPertama.javaHalo dari thread virtualKode ini tidak memuat new Thread(...) sama sekali. Pabrik Thread.ofVirtual() mengembalikan pembangun (builder). Memanggil start() pada pembangun itu meluncurkan virtual thread baru, sedangkan tipe kembaliannya tetap Thread, sehingga variabelnya dapat dipakai seperti thread biasa, misalnya untuk join(). Bagi kode yang memanggilnya, tidak ada perubahan cara kerja yang terasa.
Menjalankan seribu tugas sekaligus#
Ukuran manfaat virtual thread baru terasa saat tugasnya banyak. Program berikut meluncurkan seribu tugas virtual sekaligus, masing-masing menghitung jumlah 1 sampai 1000 dan menulis hasilnya ke slot array miliknya, lalu menunggu semuanya selesai dengan join().
public class DemoSeribuTugas {
static final int JUMLAH = 1_000;
public static void main(String[] args) throws InterruptedException {
long[] hasil = new long[JUMLAH];
Thread[] tugas = new Thread[JUMLAH];
for (int i = 0; i < JUMLAH; i++) {
int indeks = i;
tugas[i] = Thread.ofVirtual().start(() -> {
long total = 0;
for (int j = 1; j <= 1_000; j++) {
total += j;
}
hasil[indeks] = total;
});
}
for (Thread t : tugas) {
t.join();
}
long benar = 0;
for (long h : hasil) {
if (h == 500_500) {
benar++;
}
}
System.out.println("Tugas diluncurkan : " + JUMLAH);
System.out.println("Hasil hitung benar: " + benar);
}
}Tugas diluncurkan : 1000
Hasil hitung benar: 1000Keluarannya selalu sama meskipun urutan pengerjaan antartugas tidak dijamin. JVM menjadwalkan virtual thread ke carrier thread, yaitu platform thread berukuran kecil yang jumlahnya mengikuti jumlah inti CPU, seperti digambarkan berikut.
Saat satu tugas virtual menunggu I/O, JVM melepaskannya dari carrier dan menjalankan tugas lain di tempatnya. Thread sistem operasi pun tetap sibuk tanpa harus memecah logika program menjadi potongan callback yang sulit dibaca.
Executor satu tugas satu thread virtual#
Executors.newVirtualThreadPerTaskExecutor() mengemas gaya yang sama ke dalam antarmuka ExecutorService, sehingga tugas dikirim dengan submit() dan hasilnya ditampung Future. Program berikut mengirimkan dua tugas penghitungan sederhana.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class DemoExecutorVirtual {
public static void main(String[] args) throws Exception {
try (ExecutorService eksekutor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<Integer> apel = eksekutor.submit(() -> 7 * 3);
Future<Integer> jeruk = eksekutor.submit(() -> 8 * 4);
System.out.println("Apel : " + apel.get());
System.out.println("Jeruk : " + jeruk.get());
}
}
}Apel : 21
Jeruk : 32Blok try-with-resources memastikan eksekutor ditutup dan semua tugasnya diselesaikan sebelum program berlanjut. Nama metodenya memuat petunjuk penting, yaitu per task: setiap tugas yang dikirim memperoleh virtual thread barunya sendiri, tanpa penggunaan ulang thread seperti pada thread pool. Pool tetap beralasan untuk platform thread karena threadnya mahal, sedangkan virtual thread sudah murah sehingga mengumpulkannya ke dalam pool justru membuang keunggulan utamanya.
Kapan virtual thread layak dipakai#
Kandidat terbaiknya adalah tugas yang banyak menunggu, misalnya memanggil layanan jaringan, membaca berkas, atau mengakses database dalam jumlah besar secara bersamaan. Pada beban semacam itu, ribuan virtual thread yang sebagian besar tertahan tetap ringan karena yang menunggu hanyalah objek kecil di memori JVM. Sebaliknya, tugas yang murni menghitung tanpa pernah menunggu tidak memperoleh keajaiban apa pun, sebab paralelisme nyata tetap dibatasi jumlah inti CPU. Virtual thread menambah kapasitas menampung tugas yang menunggu, bukan kecepatan menghitung.
Adapun gaya kodenya tetap sinkron dan berurut seperti thread biasa, sehingga kode server yang mudah dibaca tidak lagi harus dibayar dengan ribuan thread sistem operasi. Aturan carrier thread, cacat pinning yang ditimbulkan synchronized, serta penilaian kapan platform thread masih lebih tepat dipakai dibedah pada topik pendalaman virtual thread di modul konkurensi.
Kesalahan umum#
- Mengumpulkan virtual thread ke dalam pool mengikuti kebiasaan thread pool platform. Virtual thread memang dirancang murah untuk dibuat dan dibuang, sehingga pola yang tepat adalah satu tugas satu thread virtual, bukan kumpulan thread yang dipakai ulang.
- Mengharapkan program yang murni menghitung menjadi lebih cepat. Jumlah inti CPU tetap menjadi batas paralelisme, sedangkan virtual thread hanya menambah kapasitas untuk tugas yang menunggu.
- Mencatat log dengan
getName()dan mengira namanya bermakna. Nama bawaan virtual thread kosong, sehingga identitasnya perlu diberikan lewatThread.ofVirtual().name("pemuat-1")bila jejak lognya penting.
Ringkasan#
- Virtual thread dijadwalkan oleh JVM dan murah dibuat, sedangkan platform thread dipetakan satu banding satu ke thread sistem operasi dan mahal.
- Pembuatan memakai
Thread.ofVirtual().start(...)atauExecutors.newVirtualThreadPerTaskExecutor(). - Ribuan tugas yang sebagian besar menunggu dapat ditampung serentak karena thread yang tertahan tidak menyandera thread sistem operasi.
- Virtual thread menambah kapasitas konkurensi untuk tugas yang menunggu, bukan kecepatan menghitung.
Referensi#
- JEP 444: Virtual Threads — OpenJDK — proposal finalisasi virtual thread di JDK 21 beserta aturan penjadwalannya.
- Class Thread — Oracle
— dokumentasi
ofVirtualdan pembangun thread virtualnya. - Class Executors — Oracle
— dokumentasi
newVirtualThreadPerTaskExecutordan siklus hidup eksekutor.