Membawa konteks permintaan — pengguna aktif, token otorisasi, bahasa pilihan — ke kedalaman call stack tanpa menggelembungkan parameter method adalah kebutuhan lama di aplikasi server. Jawaban klasiknya ThreadLocal: simpan nilai per thread, ambil kembali di method mana pun. Jawaban itu berhasil selama puluhan tahun, tetapi berumur buruk di era thread pool (nilai lama tertinggal dan bocor ke permintaan berikutnya) dan semakin canggung di era jutaan virtual thread. ScopedValue, difinalkan di Java 25 melalui JEP 506, menggantikannya dengan model yang lebih sehat: nilai bersifat immutable, terikat hanya selama blok scope berjalan, dan tidak pernah tertinggal setelah scope usai. Chapter ini membedah model tersebut dengan contoh teruji langsung pada JDK 25.
Masalah: ThreadLocal yang Bisa Tertinggal #
ThreadLocal bersifat mutable dan unbounded: set() dapat dipanggil kapan saja, dan nilainya bertahan sampai remove() dipanggil atau thread mati. Pada thread pool yang mendaur ulang thread antar permintaan, satu set() yang lupa dibersihkan membuat permintaan berikutnya membaca data permintaan sebelumnya — cacat keamanan yang nyata dan sulit direproduksi. Pada virtual thread yang jutaan jumlahnya, biaya rekam ThreadLocal per thread juga ikut terbawa. ScopedValue memotong kedua masalah di akarnya: nilainya tidak bisa diubah dari dalam (get() saja), dan masa hidupnya dikunci pada blok run().
Cara Kerja Pengikatan Scope #
ScopedValue.newInstance() membuat slot kosong bertipe generik. Pengikatan hanya terjadi lewat ScopedValue.where(nilai, isi).run(blok): selama blok berjalan, get() mengembalikan isi; begitu blok selesai, pengikatan lepas otomatis — tidak ada remove() yang perlu diingat. Rantai where(...).where(...) mengikat beberapa nilai sekaligus, dan pengikatan ulang (rebinding) di scope bersarang hanya berlaku di dalam scope itu. Bila get() dipanggil tanpa pengikatan, JVM melempar NoSuchElementException — kegagalan yang terdeteksi cepat, bukan nilai null yang merambat diam-diam.
Konteks Permintaan dalam Praktik #
Contoh teruji berikut memakai dua nilai terlingkup, rebinding bersarang, pemeriksaan status, dan uji pewarisan ke thread anak:
public class NilaiTerlingkup {
// Nilai terlingkup: immutable, terikat sementara oleh scope.
static final ScopedValue<String> PENGGUNA = ScopedValue.newInstance();
static final ScopedValue<Integer> KUOTA = ScopedValue.newInstance();
static void cetakLaporan() {
System.out.printf("pengguna=%s, kuota=%d%n", PENGGUNA.get(), KUOTA.get());
}
public static void main(String[] args) throws InterruptedException {
// Satu scope mengikat beberapa nilai sekaligus lewat rantai where.
ScopedValue.where(PENGGUNA, "budi").where(KUOTA, 100).run(() -> {
cetakLaporan();
// Rebinding di scope bersarang hanya berlaku di dalamnya.
ScopedValue.where(PENGGUNA, "siti").run(NilaiTerlingkup::cetakLaporan);
cetakLaporan(); // kembali ke "budi"
});
// Setelah scope berakhir, nilai tidak lagi terikat.
System.out.println("setelah scope : " + PENGGUNA.isBound());
// Thread biasa TIDAK mewarisi binding; pewarisan hanya terjadi
// lewat structured concurrency pada scope yang sama.
ScopedValue.where(PENGGUNA, "dewi").run(() -> {
Thread anak = Thread.ofVirtual().start(() ->
System.out.println("anak lihat : " + PENGGUNA.isBound()));
try {
anak.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// get() tanpa binding gagal cepat dengan exception yang jelas.
try {
PENGGUNA.get();
} catch (RuntimeException e) {
System.out.println("tanpa binding : " + e.getClass().getSimpleName());
}
}
}Output yang Dihasilkan:
pengguna=budi, kuota=100
pengguna=siti, kuota=100
pengguna=budi, kuota=100
setelah scope : false
anak lihat : false
tanpa binding : NoSuchElementExceptionBedah baris-per-baris untuk bagian pentingnya:
- Baris
static final ScopedValue<String> PENGGUNA = ScopedValue.newInstance();: slot dideklarasikanstatic final— khas idiom ScopedValue; slot adalah pegangan, bukan wadah nilai yang berubah. - Baris
ScopedValue.where(PENGGUNA, "budi").where(KUOTA, 100).run(...): dua nilai terikat bersamaan; seluruh method yang dipanggil dari dalam blok dapat membacanya tanpa parameter tambahan. - Baris
ScopedValue.where(PENGGUNA, "siti").run(...): rebinding bersarang — di dalam blok ituPENGGUNAbernilai"siti", lalu kembali ke"budi"setelahnya; isi asli tidak pernah dimodifikasi. - Baris
System.out.println("setelah scope : " + PENGGUNA.isBound()): keluar blok, pengikatan sudah lepas (false) — tidak ada pembersihan manual. - Baris
anak lihat: virtual thread biasa yang dibuat di dalam scope tidak mewarisi pengikatan; pewarisan hanya terjadi pada subtask dari structured concurrency, yang berbagi pohon scope dengan induknya.
| Aspek | ThreadLocal | ScopedValue |
|---|---|---|
| Mutabilitas | Bebas set()/remove() | Immutable, hanya get() |
| Masa hidup | Sampai thread mati atau dibersihkan | Selama blok run() |
| Risiko bocor antar permintaan | Tinggi pada thread pool | Tidak ada |
| Pewarisan ke thread anak | InheritableThreadLocal, mahal | Otomatis lewat structured concurrency |
| Akses tanpa nilai | null (merambat senyap) | NoSuchElementException |
Dengan triad Project Loom tuntas — virtual thread, structured concurrency, scoped value — satu kebiasaan kecil sintaksis masih tersisa untuk dirapikan: variabel sisa yang tidak terpakai, kini boleh ditulis sekadar _, pokok bahasan chapter berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
NilaiTerlingkup.java
Unduh berkaspublic class NilaiTerlingkup {
// Nilai terlingkup: immutable, terikat sementara oleh scope.
static final ScopedValue<String> PENGGUNA = ScopedValue.newInstance();
static final ScopedValue<Integer> KUOTA = ScopedValue.newInstance();
static void cetakLaporan() {
System.out.printf("pengguna=%s, kuota=%d%n", PENGGUNA.get(), KUOTA.get());
}
public static void main(String[] args) throws InterruptedException {
// Satu scope mengikat beberapa nilai sekaligus lewat rantai where.
ScopedValue.where(PENGGUNA, "budi").where(KUOTA, 100).run(() -> {
cetakLaporan();
// Rebinding di scope bersarang hanya berlaku di dalamnya.
ScopedValue.where(PENGGUNA, "siti").run(NilaiTerlingkup::cetakLaporan);
cetakLaporan(); // kembali ke "budi"
});
// Setelah scope berakhir, nilai tidak lagi terikat.
System.out.println("setelah scope : " + PENGGUNA.isBound());
// Thread biasa TIDAK mewarisi binding; pewarisan hanya terjadi
// lewat structured concurrency pada scope yang sama.
ScopedValue.where(PENGGUNA, "dewi").run(() -> {
Thread anak = Thread.ofVirtual().start(() ->
System.out.println("anak lihat : " + PENGGUNA.isBound()));
try {
anak.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// get() tanpa binding gagal cepat dengan exception yang jelas.
try {
PENGGUNA.get();
} catch (RuntimeException e) {
System.out.println("tanpa binding : " + e.getClass().getSimpleName());
}
}
}