Kita telah menempuh perjalanan panjang: thread dan monitor, lock dan executor, virtual threads hingga structured concurrency. Alatnya banyak, dan justru karena banyak, keputusan sehari-hari perlu dipegang oleh sedikit prinsip yang tidak berubah. Chapter penutup ini merangkai seluruh Section menjadi satu daftar praktik terbaik — bukan aturan kosong, melainkan panduan yang masing-masing lahir dari jenis kegagalan nyata yang telah kita bedah di chapter sebelumnya.

Prinsip Satu: Hindari State Bersama #

Praktik terbaik concurrency yang paling ampuh adalah tidak membutuhkannya. Kode yang tidak berbagi data mutable tidak memiliki race condition, tidak perlu lock, dan dapat diuji seperti kode biasa. Tiga bentuk konkretnya: jadikan data immutable (record, final) setiap kali mungkin; kandungkan perhitungan dalam variabel lokal per-task; dan lewatkan hasil antar tahap lewat nilai kembalian atau antrean (message passing), bukan field bersama. Framework java.util.concurrent sendiri dibangun di atas filosofi ini — CompletableFuture merangkai tahapan lewat nilai, bukan variabel bersama.

Prinsip Dua: Naik dari Alat Rendah ke Alat Tinggi #

Urutan pilihan alat sebaiknya dari yang paling deklaratif: task dan stream dulu (executor, CompletableFuture, parallel stream untuk data CPU-bound), lalu collection aman (ConcurrentHashMap, BlockingQueue), lalu variabel atomik, dan terakhir lock manual. Lock memberi kendali penuh sekaligus tanggung jawab penuh: deadlock, lupa unlock, dan kontensi datang bersamanya. Ketika terpaksa memakai lock, patuhi tiga kaidah — blok kritis seringkas mungkin, jangan pernah memanggil kode asing di dalamnya, dan unlock di finally (atau gunakan try-with-resources ReentrantLock bila tersedia).

Prinsip Tiga: Deterministik di Luar, Paralel di Dalam #

Blok pembangunan interaktif — mari kita lihat satu program kecil yang mempraktikkan seluruh prinsip di atas sekaligus:

import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;

public class RingkasanPraktik {

    // 1) state dibagi minim: hanya penghitung atomik dan antrean pesan
    private static final AtomicInteger dicatat = new AtomicInteger();
    private static final ConcurrentLinkedQueue<String> log = new ConcurrentLinkedQueue<>();

    public static void main(String[] args) throws Exception {
        List<String> kota = List.of("Jakarta", "Bandung", "Surabaya", "Medan", "Palembang");

        // 2) executor, bukan Thread mentah -- pool dikelola rapi
        try (ExecutorService pool = Executors.newFixedThreadPool(3)) {
            // 3) hasil sebagai Future yang ditunggu terurut -- tanpa println dari task
            List<Future<String>> hasil = kota.stream()
                    .map(k -> pool.submit(() -> proses(k)))
                    .toList();

            for (Future<String> f : hasil) {
                System.out.println(f.get());
            }
        } // try-with-resources: shutdown otomatis

        System.out.println("total diproses : " + dicatat.get());
    }

    private static String proses(String kota) {
        catatLog(kota);
        dicatat.incrementAndGet();
        return "laporan cuaca " + kota + " selesai";
    }

    private static void catatLog(String kota) {
        log.add("memproses " + kota);
    }
}

Output yang Dihasilkan:

text
laporan cuaca Jakarta selesai
laporan cuaca Bandung selesai
laporan cuaca Surabaya selesai
laporan cuaca Medan selesai
laporan cuaca Palembang selesai
total diproses : 5

Mari kita audit program tersebut terhadap daftar praktik:

  1. State bersama hanya dua, keduanya dari java.util.concurrent: AtomicInteger untuk penghitung dan ConcurrentLinkedQueue untuk jejak log. Tidak ada field telanjang yang ditulis banyak thread.
  2. ExecutorService dalam try-with-resources: sejak Java 19 executor mengimplementasikan AutoCloseable, sehingga close() menunggu tugas selesai dengan tertib — tidak ada shutdown() yang terlupa.
  3. Task mengembalikan nilai, tidak mencetak: seluruh hasil dikumpulkan sebagai Future<String> dan dicetak main sesuai urutan pengiriman. Paralel di dalam, deterministik di luar — pola yang sama yang kita pakai sepanjang Section ini agar keluaran dapat diverifikasi.
  4. Immutable masukan: List.of(...) dan lambda hanya membaca — tidak satu pun struktur yang dimutasi dari dalam task.

Prinsip Empat: Diagnosis Itu Keterampilan Terukur #

Ketika kode paralel berperilaku aneh, jangan menebak. Alat bawaan JDK memberi jendela langsung: jstack <pid> dan jcmd <pid> Thread.print memperlihatkan seluruh thread beserta state dan frame — deadlock dilaporkan eksplisit di bagian bawah laporan. jcmd <pid> Thread.vthread_scheduler membantu melihat scheduler virtual thread. Untuk pengujian formal, proyek jcstress dari OpenJDK menyediakan harness yang dirancang mendeteksi pelanggaran model memori — jauh lebih sahih daripada loop pengujian buatan sendiri. Rangkaian lengkap alat diagnosis ini akan kita bahas lebih dalam pada Section JVM dan Memory Management.

Prinsip Lima: Hormati Konteks yang Dipinjam #

Dua jebakan penutup yang kerap menggigit kode produksi. Pertama, ThreadLocal di dalam thread pool jarang dibersihkan — nilai dari permintaan sebelumnya bisa terbawa ke permintaan berikutnya; selalu panggil remove() di akhir pemakaian. Kedua, blocking I/O di dalam pool kecil melumpuhkan seluruh antrian; untuk beban kerja seperti itu, pilih virtual threads atau perbesar pool dengan sadar. Kedua kasus adalah turunan dari prinsip yang sama: sumber daya bersama (pool, scheduler, konteks) menuntut disiplin pengembalian.

Dengan lima prinsip ini — minim state, alat tinggi dulu, deterministik di luar, diagnosis terukur, konteks dihormati — Anda memiliki peta untuk seluruh pilihan yang akan muncul di dunia nyata. Section berikutnya melangkah keluar dari satu proses: bagaimana program Java berbicara dengan dunia lewat jaringan.

Referensi #

Berkas Sumber

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

RingkasanPraktik.java

Unduh berkas
java
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;

public class RingkasanPraktik {

    // 1) state dibagi minim: hanya penghitung atomik dan antrean pesan
    private static final AtomicInteger dicatat = new AtomicInteger();
    private static final ConcurrentLinkedQueue<String> log = new ConcurrentLinkedQueue<>();

    public static void main(String[] args) throws Exception {
        List<String> kota = List.of("Jakarta", "Bandung", "Surabaya", "Medan", "Palembang");

        // 2) executor, bukan Thread mentah -- pool dikelola rapi
        try (ExecutorService pool = Executors.newFixedThreadPool(3)) {
            // 3) hasil sebagai Future yang ditunggu terurut -- tanpa println dari task
            List<Future<String>> hasil = kota.stream()
                    .map(k -> pool.submit(() -> proses(k)))
                    .toList();

            for (Future<String> f : hasil) {
                System.out.println(f.get());
            }
        } // try-with-resources: shutdown otomatis

        System.out.println("total diproses : " + dicatat.get());
    }

    private static String proses(String kota) {
        catatLog(kota);
        dicatat.incrementAndGet();
        return "laporan cuaca " + kota + " selesai";
    }

    private static void catatLog(String kota) {
        log.add("memproses " + kota);
    }
}