Masalah berbagi data antar thread ternyata punya dua lapis yang berbeda. Lapis pertama adalah atomicity — apakah operasi berjalan utuh tanpa disisip thread lain — dan sudah kita tangani dengan lock pada Chapter sebelumnya. Lapis kedua adalah visibility: apakah tulisan yang dilakukan satu thread benar-benar terlihat oleh thread lain pada waktunya. Lapis kedua ini punya obat yang jauh lebih ringan dari lock: kata kunci volatile. Bab ini membedah apa yang volatile jamin, apa yang tidak, dan bagaimana pola pemakaian yang benar agar jaminannya benar-benar berlaku.
Masalah Visibilitas #
Java Memory Model tidak mewajibkan pembacaan variabel selalu diambil dari memori utama. Compiler JIT bebas menyimpan nilai variabel di register, dan hardware bebas menyajikan versi terbaru lewat cache core masing-masing. Optimasi ini sangat menguntungkan untuk thread tunggal, tetapi pada program konkuren menghasilkan kejadian yang membingungkan: thread A sudah menulis berhenti = true, thread B masih membaca false dan terus berputar di loop.
Bentuk klasiknya seperti ini:
private static boolean berhenti = false;
// di thread pekerja:
while (!berhenti) {
kerja(); // compiler berhak mengangkat pembacaan berhenti keluar loop
}
// di thread main:
berhenti = true; // pekerja belum tentu melihat tulisan iniTanpa koordinasi apa pun, compiler berhak menyimpulkan bahwa loop tersebut membaca nilai yang sama terus-menerus — hasil observasi yang sah menurut aturan optimasi thread-tunggal. Thread pekerja bisa jadi tidak pernah melihat tulisan dari thread main, dan program tidak pernah berhenti.
Apa yang Dijamin volatile #
Menandai variabel dengan volatile memasang dua janji khusus pada variabel tersebut:
- Visibilitas: setiap pembacaan volatile selalu mengambil tulisan volatile termutakhir; tulisan dari thread mana pun langsung tersedia bagi semua thread.
- Pagar urutan (happens-before): tulisan variabel biasa yang dilakukan sebelum sebuah tulisan volatile oleh thread yang sama dijamin terlihat oleh thread yang membaca setelah pembacaan volatile yang sama. Hubungan ini disebut happens-before dan menjadi fondasi pola bendera di bawah.
Janji kedua inilah yang membuat volatile jauh lebih berharga daripada sekadar “selalu baca dari memori”. Thread penulis menulis data dulu, bendera terakhir; thread pembaca membaca bendera dulu, data setelahnya. Begitu pembaca melihat bendera menyala, seluruh tulisan data sebelumnya ikut terlihat — tanpa lock sama sekali. Sebagai bonus kecil, pembacaan dan penulisan tunggal variabel volatile long/volatile double juga dijamin tidak terbelah menjadi dua akses 32-bit.
Pola Bendera yang Benar #
Program berikut mempraktikkan pola tersebut utuh. Thread penulis mengisi angka lalu menyalakan bendera siap; thread pembaca berputar menunggu bendera, lalu membaca angka yang dijamin utuh terlihat:
public class BenderaVolatile {
private static int angka = 0; // data biasa, non-volatile
private static volatile boolean siap = false; // bendera pengumuman
public static void main(String[] args) throws InterruptedException {
int[] hasilBaca = new int[1];
Thread pembaca = new Thread(() -> {
// Menunggu bendera menyala; onSpinWait() memberi sinyal ke CPU
// bahwa loop ini sengaja sibuk menunggu.
while (!siap) {
Thread.onSpinWait();
}
// Melihat siap == true berarti seluruh tulisan sebelum penulis
// menyalakan bendera sudah terlihat -- termasuk angka = 42.
hasilBaca[0] = angka;
}, "pembaca");
Thread penulis = new Thread(() -> {
angka = 42; // tulisan data dilakukan lebih dulu...
siap = true; // ...baru bendera dinyalakan setelah data siap
}, "penulis");
pembaca.start();
Thread.sleep(50); // biarkan pembaca sempat menunggu di loop
penulis.start();
pembaca.join();
penulis.join();
System.out.println("pembaca melihat angka : " + hasilBaca[0]);
System.out.println("pembaca melihat siap : " + siap);
}
}Output yang Dihasilkan:
pembaca melihat angka : 42
pembaca melihat siap : trueMari kita telaah mengapa hasilnya pasti seperti itu:
- Baris 3-4 (deklarasi dua field):
angkasengaja dibiarkan biasa; hanya benderasiapyang volatile. Inilah inti polanya — data menumpang pada jaminan bendera, bukan menandai seluruh field sebagai volatile. - Baris 28-29 (
angka = 42; siap = true;): urutan dua baris ini wajib. Pagar happens-before hanya melindungi tulisan yang berada di depan tulisan volatile di sisi penulis; tertukar urutannya (bendera dulu, data belakangan) membuat jaminannya kosong. - Baris 12-14 (
while (!siap) Thread.onSpinWait();): pembaca sibuk memeriksa bendera. Tanpa volatile disiap, compiler berhak membaca sekali lalu memutari hasil lama — loop tidak akan pernah selesai. - Baris 18 (
hasilBaca[0] = angka;): pembacaanangkadilakukan setelah melihatsiap == true, sehingga berada setelah pagar happens-before; nilai42dijamin terlihat, bukan kebetulan cache. - Baris 22 (
Thread.sleep(50)): hanya memberi waktu pembaca masuk ke loop — hasil akhir tidak bergantung pada durasi ini karena jaminan berasal dari happens-before, bukan dari timing.
volatile Bukan Obat untuk Atomicity #
Batasan volatile paling penting untuk diingat: visibilitas bukan atomicity. Operasi baca-hitung-tulis tetap tiga langkah terpisah walaupun setiap langkahnya melihat nilai termutakhir. Program berikut memasang jeda di antara langkah-langkah tersebut sehingga kekalahan telanjurnya terlihat pasti — meskipun counter-nya sudah volatile:
public class VolatileTidakAtomic {
// volatile menjamin setiap pembacaan melihat tulisan termutakhir,
// tetapi tidak menjadikan baca-hitung-tulis menjadi satu kesatuan.
private static volatile int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(() -> {
int baca = counter; // membaca 0
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
counter = baca + 1; // menulis 1
}, "teller-A");
Thread b = new Thread(() -> {
int baca = counter; // masih membaca 0: teller-A belum menulis
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
counter = baca + 1; // menulis 1, menimpa tulisan teller-A
}, "teller-B");
a.start();
b.start();
a.join();
b.join();
System.out.println("counter akhir : " + counter);
System.out.println("seharusnya : 2");
}
}Output yang Dihasilkan:
counter akhir : 1
seharusnya : 2Kedua teller membaca counter pada saat nilainya masih 0 — pembacaan itu memang segar, itulah yang volatile jamin. Tetapi kesegaran tidak menolong: keduanya menghitung 0 + 1, dan tulisan kedua menimpa tulisan pertama. Hasil akhirnya 1, sama seperti versi non-volatile pada Chapter Race Conditions. Kesimpulannya satu kalimat: volatile menyembuhkan data basi, bukan penyisipan operasi.
Kapan volatile Cukup #
Pola yang cocok untuk volatile memiliki ciri tetap: satu penulis, banyak pembaca, dan nilai yang sederhana bentuk statusnya. Contoh nyatanya meliputi bendera status (berjalan, dibatalkan), pengaturan yang dimuat sekali lalu dibaca semua thread, atau hasil komputasi yang diumumkan sekali di akhir. Begitu ada dua thread yang sama-sama menulis, atau nilai harus dihitung dari nilai lama (counter++, saldo -= jumlah), volatile salah alat.
volatile memberi kita jaminan visibilitas tanpa biaya antrean lock. Pertanyaan berikutnya alami: bagaimana bila kita ingin keduanya sekaligus — operasi utuh dan bebas lock eksplisit? Keluarga class Atomic di java.util.concurrent.atomic menjawabnya dengan trik hardware bernama CAS, topik Chapter berikutnya.
Referensi #
- Oracle: The Java Tutorials — Atomic Access
- Oracle: Java Language Specification — Memory Model (Chapter 17)
- Oracle: Class Thread (onSpinWait)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BenderaVolatile.java
Unduh berkaspublic class BenderaVolatile {
private static int angka = 0; // data biasa, non-volatile
private static volatile boolean siap = false; // bendera pengumuman
public static void main(String[] args) throws InterruptedException {
int[] hasilBaca = new int[1];
Thread pembaca = new Thread(() -> {
// Menunggu bendera menyala; onSpinWait() memberi sinyal ke CPU
// bahwa loop ini sengaja sibuk menunggu.
while (!siap) {
Thread.onSpinWait();
}
// Melihat siap == true berarti seluruh tulisan sebelum penulis
// menyalakan bendera sudah terlihat -- termasuk angka = 42.
hasilBaca[0] = angka;
}, "pembaca");
Thread penulis = new Thread(() -> {
angka = 42; // tulisan data dilakukan lebih dulu...
siap = true; // ...baru bendera dinyalakan setelah data siap
}, "penulis");
pembaca.start();
Thread.sleep(50); // biarkan pembaca sempat menunggu di loop
penulis.start();
pembaca.join();
penulis.join();
System.out.println("pembaca melihat angka : " + hasilBaca[0]);
System.out.println("pembaca melihat siap : " + siap);
}
}
VolatileTidakAtomic.java
Unduh berkaspublic class VolatileTidakAtomic {
// volatile menjamin setiap pembacaan melihat tulisan termutakhir,
// tetapi tidak menjadikan baca-hitung-tulis menjadi satu kesatuan.
private static volatile int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(() -> {
int baca = counter; // membaca 0
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
counter = baca + 1; // menulis 1
}, "teller-A");
Thread b = new Thread(() -> {
int baca = counter; // masih membaca 0: teller-A belum menulis
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
counter = baca + 1; // menulis 1, menimpa tulisan teller-A
}, "teller-B");
a.start();
b.start();
a.join();
b.join();
System.out.println("counter akhir : " + counter);
System.out.println("seharusnya : 2");
}
}