Konkurensi yang benar-benar menolong
Beban kerja mana yang berubah oleh virtual thread, kenapa throughput kadang tidak bergerak sama sekali, untuk apa StructuredTaskScope, dan di mana CompletableFuture masih alat yang lebih tepat.
Virtual thread membuat thread-per-request jadi murah lagi. Dia tidak membuat apa pun jadi lebih cepat.
Apa yang dia ubah
Platform thread adalah thread OS: stack sekitar satu megabyte, dijadwalkan kernel, cukup mahal sampai harus di-pool. Virtual thread dikelola JVM, biayanya beberapa ratus byte, dan saat dia memblokir dia di-unmount dari carrier thread-nya sehingga carrier itu bisa menjalankan hal lain.
Jadi beban kerja yang dia tolong adalah I/O yang memblokir: request yang hidupnya dihabiskan menunggu database, panggilan HTTP, atau queue. Sepuluh ribu request seperti itu bisa jadi sepuluh ribu virtual thread di atas segelintir carrier.
Beban kerja yang tidak dia tolong adalah kerja CPU-bound. Enam belas thread yang menghitung di enam belas core sudah memakai mesinnya; menjadikannya virtual tidak mengubah apa pun selain pembukuan. Kalau sebuah service ada di 100% CPU, virtual thread bukan jawabannya — tidak ada penungguan yang bisa direklamasi.
Kenapa throughput kadang tidak bergerak
Kamu memindahkan service thread-per-request ke virtual thread dan angkanya nyaris tidak berubah. Tiga penyebab yang biasa, dalam urutan yang layak diperiksa:
Bottleneck-nya sejak awal bukan thread. Connection pool sepuluh berarti sepuluh query bersamaan, berapa pun jumlah thread-nya. Sepuluh ribu virtual thread yang semuanya menunggu sepuluh koneksi menghasilkan throughput yang sama seperti sebelumnya, ditambah antrean yang lebih panjang. Ini yang paling sering, dan itu sebabnya memperbesar pool thread tanpa memperbesar yang ditungguinya tidak menghasilkan apa pun.
Pinning. Virtual thread yang memblokir di dalam blok synchronized tidak
bisa di-unmount — dia menyandera carrier-nya selama itu. JDK terbaru sudah
menghilangkan sebagian besar kasus ini, tapi method synchronized yang
membungkus panggilan I/O tetap bentuk yang perlu dicari. ReentrantLock tidak
mem-pin.
Panggilan native. Memblokir di kode native juga tidak bisa di-unmount.
StructuredTaskScope untuk fan-out
Masalah menjalankan beberapa panggilan bersamaan secara manual bukan di menjalankannya, tapi di pembersihannya: kalau satu gagal, siapa yang membatalkan sisanya, dan siapa menunggu apa.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Subtask<Profile> profile = scope.fork(() -> fetchProfile(id)); Subtask<Orders> orders = scope.fork(() -> fetchOrders(id));
scope.join().throwIfFailed(); // dua-duanya selesai, atau kegagalan pertama menang return new Dashboard(profile.get(), orders.get());}Scope-nya memiliki subtask-subtask itu. Kalau salah satu gagal, yang lain dibatalkan; kalau thread pembungkusnya di-interrupt, dua-duanya dibatalkan. Masa hidup konkurensinya adalah masa hidup bloknya — itu inti dari namanya, dan alasan task yang bocor tidak mungkin terjadi di sini seperti pada executor telanjang.
Di mana CompletableFuture masih menang
Virtual thread membuat kode yang memblokir jadi murah, jadi sebagian besar
fan-out menjadi lebih sederhana sebagai panggilan blocking biasa di dalam scope.
CompletableFuture tetap alat yang lebih tepat ketika komposisinya sendiri
adalah logikanya:
- pipeline di mana setiap tahap mentransformasi hasil sebelumnya, dan kamu ingin itu tertulis sebagai rantai alih-alih deretan assignment
- menggabungkan hasil dengan timeout dan fallback per tahap
- API yang bukan milikmu dan sudah mengembalikan future
Alasan “pakai CompletableFuture supaya tidak memblokir thread” itulah yang
sekarang tidak berlaku lagi.
Parallel stream: sisi tajamnya
list.parallelStream().map(this::callService).toList();Secara default ini jalan di ForkJoinPool common, yang dipakai bersama oleh
seluruh JVM. Taruh I/O yang memblokir di situ dan kamu membuat kelaparan semua
hal lain yang memakainya — termasuk parallel stream lain dan sebagian internal
library. Parallel stream itu untuk kerja CPU-bound di atas sumber yang besar dan
murah dibelah. Untuk fan-out I/O, pakai scope atau executor milikmu sendiri.
Dan parallel() tidak membuat stream kecil jadi lebih cepat. Membelah,
menjadwalkan, dan menggabungkan lebih mahal daripada kerjanya sendiri ketika
sumbernya cuma beberapa ratus elemen.
Dua fakta kecil yang sering mengecoh
Stream itu sekali pakai: konsumsi dua kali dan kamu dapat
IllegalStateException: stream has already been operated upon or closed. Dan
stream tidak melakukan apa pun sampai operasi terminal dijalankan — map dengan
efek samping tanpa collect adalah no-op yang terlihat seperti kode.
Sisi produksinya, dengan penentuan ukuran pool yang benar-benar berpengaruh: Bertahan di Produksi dengan Java.
Diskusi
Memuat komentar…