Counter dari Chapter Race Conditions akhirnya aman setelah diberi synchronized, tetapi keamanan itu dibayar dengan antrean: setiap thread harus menunggu giliran memegang monitor meskipun pekerjaannya hanya menambah satu angka. Paket java.util.concurrent.atomic menawarkan jalan lain — operasi gabungan seperti “baca, tambah, tulis” yang dijalankan sebagai satu kesatuan tanpa lock sama sekali, berbekal instruksi khusus di prosesor modern. Bab ini memperkenalkan keluarga class Atomic, membedah mesin di baliknya bernama compare-and-swap (CAS), dan menutup dengan LongAdder untuk kasus kontensi paling padat.

Keluarga Class Atomic #

Paket java.util.concurrent.atomic menyediakan pembungkus untuk tipe-tipe yang paling sering diperebutkan: AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference<V>, hingga varian array seperti AtomicIntegerArray. Seluruhnya memiliki method operasi gabungan yang dijamin atomik — selesai seolah satu langkah tak terpisahkan:

  • incrementAndGet() / decrementAndGet() — tambah/kurang satu, kembalikan nilai baru.
  • getAndAdd(n) — kembalikan nilai lama, lalu tambah n.
  • addAndGet(n) — tambah n, lalu kembalikan nilai baru.
  • getAndSet(n) — kembalikan nilai lama, ganti dengan n.
  • compareAndSet(dugaan, nilaiBaru) — tukar hanya bila nilai sekarang sama dengan dugaan.

Program berikut menguji ketahanannya di bawah dua thread sekaligus mendemokan semantik beberapa method di atas:

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

public class CounterAtomic {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger nilai = new AtomicInteger(0);

        Runnable tugas = () -> {
            for (int i = 0; i < 100_000; i++) {
                nilai.incrementAndGet(); // atomik: baca + tambah + tulis satu kesatuan
            }
        };
        Thread a = new Thread(tugas, "teller-A");
        Thread b = new Thread(tugas, "teller-B");
        a.start();
        b.start();
        a.join();
        b.join();

        // Tiga operasi atomik lain, dijalankan thread main setelah join:
        int lama = nilai.getAndAdd(500);  // kembalikan nilai lama, lalu tambah 500
        int baru = nilai.addAndGet(10);   // tambah 10, lalu kembalikan nilai baru
        int sekarang = nilai.get();

        System.out.println("incrementAndGet 2 x 100000   : " + lama);
        System.out.println("addAndGet(10) nilai baru     : " + baru);
        System.out.println("nilai akhir                  : " + sekarang);

        // LongAdder: pecahan counter untuk kontensi sangat tinggi.
        LongAdder jumlah = new LongAdder();
        Runnable tugasAdder = () -> {
            for (int i = 0; i < 100_000; i++) {
                jumlah.increment();
            }
        };
        Thread c = new Thread(tugasAdder, "teller-C");
        Thread d = new Thread(tugasAdder, "teller-D");
        c.start();
        d.start();
        c.join();
        d.join();
        System.out.println("LongAdder sum() 2 x 100000   : " + jumlah.sum());
    }
}

Output yang Dihasilkan:

text
incrementAndGet 2 x 100000   : 200000
addAndGet(10) nilai baru     : 200510
nilai akhir                  : 200510
LongAdder sum() 2 x 100000   : 200000

Mari kita telaah baris-baris kode di atas:

  1. Baris 12 (nilai.incrementAndGet()): dua thread masing-masing memanggilnya 100.000 kali tanpa lock, tanpa synchronized — total pasti 200000, hasil yang selalu benar karena setiap panggilan adalah operasi atomik tunggal.
  2. Baris 23 (int lama = nilai.getAndAdd(500);): getAndAdd mengembalikan nilai sebelum penambahan (200000) sambil mengubah nilai menjadi 200500 — nilai kembaliannya yang menegaskan semantik “get dulu, add kemudian”.
  3. Baris 24 (int baru = nilai.addAndGet(10);): kebalikannya — tambah dulu (200510), kembalikan hasil sesudahnya. Nilai baru dan sekarang sama karena tidak ada thread lain yang ikut menulis lagi.
  4. Baris 36 (LongAdder jumlah = new LongAdder();): LongAdder dipakai seperti counter biasa; sum() menggabungkan seluruh pecahannya menjadi total 200000.

Mekanisme Compare and Swap #

Kekuatan di balik seluruh method tersebut adalah satu operasi primitif: compare-and-swap. Method compareAndSet(dugaan, nilaiBaru) memerintahkan prosesor: baca nilai sekarang; bila sama dengan dugaan, tulis nilaiBaru dan laporkan berhasil; bila berbeda, jangan tulis apa pun dan laporkan gagal. Penbandingan dan penulisan terjadi sebagai satu langkah hardware yang tidak bisa disisip thread lain — prosesor modern menyediakannya lewat instruksi khusus (keluarga cmpxchg pada arsitektur x86, misalnya).

Program berikut menguji kontraknya secara langsung, lalu menggunakannya dalam pola retry loop untuk memperbarui nilai tertinggi tanpa lock:

java PolaCAS.java
import java.util.concurrent.atomic.AtomicInteger;

public class PolaCAS {
    public static void main(String[] args) throws InterruptedException {
        // ---- Bagian 1: kontrak compareAndSet(expected, update) ----
        AtomicInteger nilai = new AtomicInteger(100);
        boolean sukses1 = nilai.compareAndSet(100, 250); // nilai memang 100
        boolean sukses2 = nilai.compareAndSet(100, 999); // nilai sudah 250
        System.out.println("CAS(100 -> 250) berhasil : " + sukses1);
        System.out.println("CAS(100 -> 999) berhasil : " + sukses2);
        System.out.println("nilai sekarang           : " + nilai.get());

        // ---- Bagian 2: pola retry loop tanpa lock ----
        AtomicInteger tertinggi = new AtomicInteger(0);
        int[] penawaran = {1500, 900, 2300, 1750};

        Thread[] pelelang = new Thread[penawaran.length];
        for (int i = 0; i < penawaran.length; i++) {
            int target = penawaran[i];
            pelelang[i] = new Thread(() -> {
                while (true) {
                    int lama = tertinggi.get();
                    if (target <= lama) {
                        return; // penawaran kita kalah: tidak ada yang perlu ditukar
                    }
                    if (tertinggi.compareAndSet(lama, target)) {
                        return; // berhasil menjadi tertinggi yang baru
                    }
                    // CAS gagal karena thread lain mengubah lebih dulu: ulangi.
                }
            }, "pelelang-" + (i + 1));
            pelelang[i].start();
        }
        for (Thread t : pelelang) {
            t.join();
        }
        System.out.println("harga tertinggi akhir    : " + tertinggi.get());
    }
}

Output yang Dihasilkan:

text
CAS(100 -> 250) berhasil : true
CAS(100 -> 999) berhasil : false
nilai sekarang           : 250
harga tertinggi akhir    : 2300

Mari kita telaah dua bagian tersebut:

  1. Bagian 1 (baris 9-11): CAS pertama berhasil karena nilai sekarang (100) sama dengan dugaan; nilai menjadi 250. CAS kedua gagal karena dugaan 100 sudah tidak cocok — nilai dibiarkan 250, tidak tersentuh setengah jalan. Inilah bedanya CAS dengan penulisan buta: CAS hanya menulis bila dunia masih sesuai asumsi kita.
  2. Baris 31 (int lama = tertinggi.get();): pola selalu dimulai dengan membaca keadaan saat ini sebagai “dugaan”.
  3. Baris 32-34 (if (target <= lama) return;): penawaran yang lebih rendah tidak perlu bertindak — cek dulu, agar CAS tidak mengganti nilai dengan yang lebih kecil.
  4. Baris 35-36 (tertinggi.compareAndSet(lama, target)): upaya tukar tanpa lock. Bila thread lain menang lebih dulu, CAS mengembalikan false dan loop membaca ulang keadaan termutakhir lalu mencoba lagi — inilah retry loop, bangunan dasar semua algoritma lock-free.
  5. Baris 46 (tertinggi.get()): hasil akhir 2300 — penawaran terbesar, tercapai lewat berapa kali pun persaingan terjadi, tanpa satu baris synchronized pun.

LongAdder untuk Kontensi Tinggi #

AtomicLong memaksa seluruh penulis berebut satu sel memori. LongAdder menyiasatnya dengan memecah total ke beberapa sel yang dipilih secara dinamis: penulis-penulis sibuk menambah ke sel yang berbeda, dan sum() menjumlahkan semuanya saat kita meminta laporan. Seluruh thread jarang menabrak sel yang sama, sehingga throughput tetap tinggi meski jumlah thread besar. Konsekuensinya: sum() hanyalah potret gabungan pada satu saat — bukan operasi atomik yang membekukan seluruh penambahan — sehingga cocok untuk statistik, hitungan request, dan metrik, tetapi bukan untuk nilai yang menjadi dasar keputusan benar-salah.

Pilihan Alat untuk Counter #

AspeksynchronizedAtomicIntegerLongAdder
CakupanBlok kode berapa pun panjangnyaSatu variabelSatu angka akumulasi
MekanismeAntrean monitorCAS pada satu selSel ganda + penjumlahan
Kinerja kontensi rendahCukupSangat baikBaik
Kinerja kontensi tinggiMenurun (antre panjang)Menurun (CAS saling membatalkan)Terbaik untuk statistik
get() selalu potret konsistenYa, bila dibaca di dalam lockYaTidak — sum() adalah agregat

Atomic variables menutup kebutuhan satu-variabel dengan elegan, tetapi begitu koordinasi melampaui satu angka — dua langkah harus berurutan, satu resource dipegang lama, atau urutan masuk harus adil — kita kembali membutuhkan lock sungguhan. synchronized memang layak, namun ReentrantLock menawarkan kendali yang lebih kaya: tryLock dengan batas waktu, interruptibility, dan fairness. Chapter berikutnya membahas semuanya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

CounterAtomic.java

Unduh berkas
java
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

public class CounterAtomic {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger nilai = new AtomicInteger(0);

        Runnable tugas = () -> {
            for (int i = 0; i < 100_000; i++) {
                nilai.incrementAndGet(); // atomik: baca + tambah + tulis satu kesatuan
            }
        };
        Thread a = new Thread(tugas, "teller-A");
        Thread b = new Thread(tugas, "teller-B");
        a.start();
        b.start();
        a.join();
        b.join();

        // Tiga operasi atomik lain, dijalankan thread main setelah join:
        int lama = nilai.getAndAdd(500);  // kembalikan nilai lama, lalu tambah 500
        int baru = nilai.addAndGet(10);   // tambah 10, lalu kembalikan nilai baru
        int sekarang = nilai.get();

        System.out.println("incrementAndGet 2 x 100000   : " + lama);
        System.out.println("addAndGet(10) nilai baru     : " + baru);
        System.out.println("nilai akhir                  : " + sekarang);

        // LongAdder: pecahan counter untuk kontensi sangat tinggi.
        LongAdder jumlah = new LongAdder();
        Runnable tugasAdder = () -> {
            for (int i = 0; i < 100_000; i++) {
                jumlah.increment();
            }
        };
        Thread c = new Thread(tugasAdder, "teller-C");
        Thread d = new Thread(tugasAdder, "teller-D");
        c.start();
        d.start();
        c.join();
        d.join();
        System.out.println("LongAdder sum() 2 x 100000   : " + jumlah.sum());
    }
}

PolaCAS.java

Unduh berkas
java
import java.util.concurrent.atomic.AtomicInteger;

public class PolaCAS {
    public static void main(String[] args) throws InterruptedException {
        // ---- Bagian 1: kontrak compareAndSet(expected, update) ----
        AtomicInteger nilai = new AtomicInteger(100);
        boolean sukses1 = nilai.compareAndSet(100, 250); // nilai memang 100
        boolean sukses2 = nilai.compareAndSet(100, 999); // nilai sudah 250
        System.out.println("CAS(100 -> 250) berhasil : " + sukses1);
        System.out.println("CAS(100 -> 999) berhasil : " + sukses2);
        System.out.println("nilai sekarang           : " + nilai.get());

        // ---- Bagian 2: pola retry loop tanpa lock ----
        AtomicInteger tertinggi = new AtomicInteger(0);
        int[] penawaran = {1500, 900, 2300, 1750};

        Thread[] pelelang = new Thread[penawaran.length];
        for (int i = 0; i < penawaran.length; i++) {
            int target = penawaran[i];
            pelelang[i] = new Thread(() -> {
                while (true) {
                    int lama = tertinggi.get();
                    if (target <= lama) {
                        return; // penawaran kita kalah: tidak ada yang perlu ditukar
                    }
                    if (tertinggi.compareAndSet(lama, target)) {
                        return; // berhasil menjadi tertinggi yang baru
                    }
                    // CAS gagal karena thread lain mengubah lebih dulu: ulangi.
                }
            }, "pelelang-" + (i + 1));
            pelelang[i].start();
        }
        for (Thread t : pelelang) {
            t.join();
        }
        System.out.println("harga tertinggi akhir    : " + tertinggi.get());
    }
}