Konteks dan pertanyaan
Pertanyaan coding dan konkurensi ini cocok untuk peran backend Java, platform, dan performa. Pertanyaan ini meminta Anda membandingkan platform thread, virtual thread, dan callback asinkron sambil mempertimbangkan konkurensi, CPU, I/O pemblokir, pool hilir, dan diagnostik secara bersamaan. Aturan pengambilan keputusan yang dapat digunakan kembali jauh lebih penting daripada sekadar menghafal nama-nama API.
Hal yang dinilai oleh pewawancara
- Apakah Anda menjelaskan bahwa virtual thread meningkatkan konkurensi yang skalabel dan throughput, bukan kecepatan eksekusi tugas tunggal.
- Apakah Anda mengidentifikasi pinning dari
synchronizedatau panggilan native serta batas yang disebabkan oleh pekerjaan yang terikat CPU (CPU-bound). - Apakah Anda menerapkan fan-out dengan satu virtual thread per tugas dan membatasi sumber daya hilir yang langka menggunakan semaphore atau pool.
- Apakah Anda merancang validasi menggunakan JFR, thread dump, latensi, utilisasi carrier, dan waktu tunggu hilir.
Pertanyaan klarifikasi sebelum menjawab
Tanyakan apakah permintaan sebagian besar menunggu pekerjaan jaringan, database, atau CPU; apakah ketiga panggilan tersebut dapat berjalan secara paralel; dan batasan konkurensi serta koneksi apa yang diterapkan oleh layanan hilir. Konfirmasikan apakah framework dan driver mendukung API pemblokir, apakah ada bagian synchronized atau native yang panjang, apakah targetnya adalah p99 yang lebih rendah atau throughput yang lebih tinggi, dan apakah executor lama dapat dipertahankan selama masa canary.
Kerangka jawaban 30 detik
Virtual thread mempertahankan kode I/O pemblokir yang mudah dipahami dan melepaskan carrier saat menunggu, sehingga sangat cocok untuk permintaan yang padat I/O. Virtual thread tidak mempercepat kode CPU atau membuat koneksi database maupun kuota hilir baru. Saya akan menggunakan satu virtual thread per tugas fan-out, semaphore atau connection pool untuk sumber daya yang terbatas, serta memeriksa lock dan panggilan native untuk mendeteksi pinning. Saya akan membuktikan hasil migrasi dengan JFR, thread dump, waktu tunggu hilir, dan perbandingan p99/throughput daripada berasumsi bahwa mengganti pool secara otomatis akan lebih cepat.
Solusi langkah demi langkah
1. Definisikan manfaatnya sebagai konkurensi selama masa tunggu
Platform thread tetap terikat pada OS thread saat menunggu I/O; virtual thread dapat ditangguhkan selama I/O pemblokir sementara carrier-nya menjalankan virtual thread lain. Ini cocok untuk layanan yang permintaannya menghabiskan sebagian besar waktu menunggu jaringan atau database. Kode yang terikat CPU tetap tunduk pada batasan core dan scheduler; virtual thread tidak mengurangi kompleksitas algoritma atau waktu CPU per permintaan.
2. Buat satu virtual thread per tugas, bukan pool virtual thread
Virtual thread adalah representasi tugas yang ringan. Gunakan Executors.newVirtualThreadPerTaskExecutor() untuk membuat satu thread bagi setiap tugas yang dikirimkan. Pool virtual thread yang ukurannya tetap justru akan memicu kembali masalah antrean tanpa menggunakan ulang carrier yang langka; batasi sumber daya eksternal daripada membuat pool untuk virtual thread.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<Profile> profile = executor.submit(() -> profileClient.fetch(id));
Future<Orders> orders = executor.submit(() -> orderClient.fetch(id));
Future<Quota> quota = executor.submit(() -> quotaClient.fetch(id));
return merge(profile.get(), orders.get(), quota.get());
}3. Batasi konkurensi hilir dengan sinyal khusus
Jumlah virtual thread bisa sangat banyak, namun koneksi database, QPS penyedia layanan, dan file descriptor tetap terbatas. Lindungi panggilan yang terbatas dengan Semaphore, atau andalkan database pool yang ada sebagai batas konkurensi; jangan membebani ukuran thread pool tetap untuk menangani penggunaan ulang thread sekaligus pembatasan sumber daya. Lepaskan permit di dalam finally, dengan batas waktu (deadline) dan kebijakan pembatalan untuk waktu tunggu.
4. Identifikasi pinning dan pekerjaan yang tidak dapat dilepas (non-unmountable)
Virtual thread dapat tetap terpasang pada carrier-nya saat memblokir di dalam synchronized atau panggilan native/foreign. Lock dalam memori yang singkat biasanya tidak masalah; lock I/O yang panjang akan menahan carrier dan mengurangi throughput. Ganti monitor di sekitar pekerjaan pemblokir pada jalur kritis dengan ReentrantLock yang sesuai, dipandu oleh event pinning JFR atau diagnostik, bukan dengan mengganti setiap blok synchronized secara membabi buta.
5. Tangani pembatalan, batas waktu, dan propagasi error
Ketiga panggilan fan-out memerlukan satu batas waktu (deadline) permintaan bersama. Ketika sebuah panggilan kritis gagal, batalkan pekerjaan yang masih menunggu agar kapasitas hilir tidak terbuang setelah klien mengalami timeout. Virtual thread mengubah kepemilikan thread; mereka tidak secara otomatis membatalkan Future, menutup body respons, atau melepaskan koneksi. Petakan interupsi, timeout, dan kegagalan hilir ke fallback eksplisit dan bersihkan sumber daya di finally.
6. Buktikan hasilnya dengan metrik dan kelompok kontrol
Bandingkan lalu lintas yang setara pada throughput, p50/p99, CPU, paralelisme carrier, jumlah virtual thread, antrean connection pool, antrean semaphore, dan error. Catat jdk.VirtualThreadPinned serta event mulai/selesai dengan JFR; periksa stack trace dengan thread dump jcmd. Jalankan beban kerja terpisah untuk tunggu I/O, beban CPU, pembatasan rate hilir, dan perebutan lock. Peningkatan pada kasus I/O namun tidak pada kasus CPU adalah hasil yang diharapkan.
Contoh jawaban berkualitas tinggi
Saya tidak akan memperlakukan virtual thread sebagai thread pool yang lebih cepat. Mereka cocok untuk layanan yang permintaannya menghabiskan sebagian besar waktu dalam I/O pemblokir karena virtual thread dapat menangguhkan eksekusi dan membebaskan carrier-nya; pekerjaan yang terikat CPU tetap dibatasi oleh jumlah core. Untuk tiga panggilan hilir paralel, saya akan menggunakan executor virtual thread per tugas dengan satu deadline permintaan dan pembatalan; connection pool atau semaphore akan menegakkan batasan database dan penyedia layanan. Saya akan memeriksa driver, lock, dan panggilan native agar I/O yang lama tidak berada di dalam synchronized dan mem-pin carrier. Rilis canary akan membandingkan throughput, p99, utilisasi carrier, waktu tunggu hilir, event pinning JFR, dan thread dump sebelum memperluas migrasi.
Kesalahan umum
- Mengatakan bahwa virtual thread membuat kode CPU lebih cepat → virtual thread terutama meningkatkan konkurensi untuk pekerjaan yang menunggu dan tidak menambah core CPU → ukur throughput, latensi, dan penggunaan CPU secara terpisah.
- Membuat pool virtual thread yang tetap → ini mencampuradukkan thread pooling dengan pembatasan sumber daya → buat satu virtual thread per tugas dan gunakan semaphore atau pool di sisi hilir.
- Mengganti setiap blok
synchronized→ critical section dalam memori yang singkat tidak selalu berbahaya, dan penggantian tanpa analisis menambah kompleksitas → ubah jalur lock pemblokir yang didukung oleh bukti JFR. - Mengabaikan batas koneksi hilir → virtual thread yang lebih banyak tidak serta-merta menambah koneksi atau kuota → tentukan alokasi beban, deadline, dan perilaku fallback.
- Hanya menjalankan uji beban dengan konkurensi tinggi → pinning, kebocoran pembatalan, atau saturasi CPU bisa tetap tersembunyi → uji kasus I/O, CPU, lock, limitasi, dan pemulihan secara terpisah.
Pertanyaan lanjutan dan jawabannya
Apa konsekuensi/kompromi (trade-off) antara virtual thread dan pemrograman reaktif?
Virtual thread mempertahankan kode pemblokir imperatif dan diagnostik yang sudah familier, yang sangat cocok untuk layanan padat I/O yang membutuhkan stack trace yang mudah dibaca. Kode reaktif mungkin cocok untuk komposisi event-stream, jumlah koneksi yang sangat tinggi, atau ekosistem non-blocking yang sudah mapan. Pilihlah berdasarkan dukungan driver, biaya pemeliharaan, target latensi, dan kemampuan observabilitas, bukan dari asumsi bahwa yang lebih baru pasti lebih cepat.
Bagaimana cara membatasi satu penyedia layanan hingga sepuluh panggilan bersamaan?
Buat Semaphore dengan sepuluh izin (permits), lakukan acquire sebelum memanggil penyedia layanan, dan lepaskan di finally; batasi waktu tunggu izin dengan deadline permintaan dan kembalikan fallback atau hasil antrean saat timeout. Jika connection pool yang ada sudah menjadi batas sebenarnya, gunakan pool tersebut alih-alih menambahkan batasan kedua.
Mengapa kelaparan carrier (carrier starvation) masih bisa terjadi?
Pemblokiran synchronized yang lama, panggilan native, tugas berat CPU, atau waktu tunggu eksternal yang tidak dibatasi dapat menahan carrier atau menghabiskan kapasitas paralelisme. Periksa event pinning JFR, thread dump, profil CPU, dan waktu tunggu hilir untuk membedakan antara pinning dan antrean biasa.
Bisakah virtual thread menggantikan pool koneksi database?
Tidak. Virtual thread bertugas mengeksekusi tugas; koneksi database adalah sumber daya eksternal yang terbatas. Tetap gunakan pool, deadline, batasan transaksi, dan metrik tunggu pool. Membiarkan virtual thread tanpa batas menunggu koneksi hanya akan memindahkan beban tekanan ke memori dan batas waktu permintaan.
Bagaimana cara membuktikan bahwa migrasi tidak memperburuk latensi?
Pertahankan input yang konstan, distribusi respons hilir, dan tingkat error. Bandingkan canary lama dan canary virtual-thread pada p50/p99, throughput, CPU, utilisasi carrier, waktu tunggu pool, penyelesaian pembatalan, dan event pinning. Sertakan skenario kondisi stabil, lonjakan tiba-tiba, hilir yang lambat, kehabisan pool, dan restart, dengan menetapkan ambang batas rollback sebelum memperluas lalu lintas.