Menjalankan banyak tugas bersamaan dengan aman: Thread dan Runnable, sinkronisasi dengan synchronized dan volatile, Executors untuk pool thread, lalu virtual threads dan structured concurrency sebagai revolusi Java modern untuk thread safety.

Sampai episode 16, semua kode kita berjalan sekuensial — satu baris, lalu baris berikutnya. Episode ini membuka dunia paralel: concurrency. Aplikasi nyata — web server, batch processing, sistem yang melayani ribuan pengguna — bergantung pada kemampuan menjalankan banyak pekerjaan bersamaan.
Mengapa concurrency adalah topik yang menantang? Karena saat banyak thread menyentuh data yang sama, muncul kelas bug baru: race condition, data rusak yang hanya muncul sesekali dan sulit direproduksi. Episode ini mengajarkan mekanisme threading Java, cara melindungi shared state, dan lompatan besar Java modern: virtual threads yang membuat ribuan thread menjadi murah.
Thread adalah unit eksekusi terkecil. Dua cara membuatnya — Runnable (behavior) + Thread (eksekusi):
public class Main {
public static void main(String[] args) {
Runnable tugas = () -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread: " + i);
}
};
Thread thread = new Thread(tugas);
thread.start();
System.out.println("Main selesai");
}
}Perhatikan start() — bukan run(). start() menciptakan thread baru yang berjalan paralel; run() langsung mengeksekusi di thread pemanggil. Output urutannya tidak menentu — itulah sifat concurrency.
Concurrency bukan untuk membuat kode terlihat canggih. Tujuannya:
Ketika dua thread membaca lalu menulis state yang sama, hasilnya tidak terduga:
public class Counter {
private int nilai = 0;
public void tambah() {
nilai++; // bukan operasi atomik!
}
public int getNilai() {
return nilai;
}
}nilai++ terlihat satu langkah, tetapi di level mesin terdiri dari baca → tambah → tulis. Dua thread bisa membaca nilai yang sama lalu menulis hasil yang sama — total yang benar hilang.
Blok/mehod synchronized mengunci agar hanya satu thread yang masuk pada satu waktu:
public class Counter {
private int nilai = 0;
public synchronized void tambah() {
nilai++;
}
public synchronized int getNilai() {
return nilai;
}
}Dengan synchronized, tiga langkah nilai++ menjadi atomik terhadap thread lain. Harganya: thread lain harus menunggu — jadi jangan mengunci lebih lama dari yang dibutuhkan.
Ada masalah lain yang lebih halus: caching. Thread bisa memegang salinan field di cache CPU-nya, sehingga perubahan thread lain tidak terlihat. volatile memaksa pembacaan/penulisan langsung ke memori utama:
public class Berhenti {
private volatile boolean stop = false;
public void hentikan() {
stop = true;
}
public void jalankan() {
while (!stop) {
// proses terus
}
}
}Kapan memakai volatile? Saat field dibaca banyak thread dan ditulis satu thread, dan penulisannya independen (bukan operasi read-modify-write seperti ++). Untuk update read-modify-write, tetap butuh synchronized atau atomics.
Membuat thread manual per tugas boros — pembuatan thread mahal. Executors menyediakan thread pool yang dipakai ulang:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 20; i++) {
final int tugas = i;
executor.submit(() -> {
System.out.println("Tugas " + tugas + " di thread " +
Thread.currentThread().getName());
});
}
executor.shutdown();
}
}newFixedThreadPool(4) membuat pool 4 thread yang dipakai bergantian untuk 20 tugas — thread dibuat sekali, bukan 20 kali. shutdown() menutup pool setelah semua tugas selesai. Aturan praktis: jangan pernah membuat thread manual untuk tugas berulang — selalu gunakan executor.
Thread platform (OS thread) mahal: setiap thread memakan ~1MB stack. Aplikasi yang butuh seribu koneksi bersamaan — ribuan thread — membuat JVM dan OS kewalahan. Pola lama mengatasinya dengan async/event-loop yang kompleks dan sulit dibaca.
Virtual threads (stabil sejak JDK 21) membalik paradigma: thread menjadi murah — JVM mengelola ribuan bahkan jutaan virtual thread di atas segelintir thread platform. Saat virtual thread menunggu I/O, JVM otomatis melepas thread platform-nya untuk tugas lain.
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
public class Main {
public static void main(String[] args) throws Exception {
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
final int tugas = i;
executor.submit(() -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Virtual thread " + tugas);
});
}
}
}
}10.000 tugas dengan sleep — dengan thread platform ini akan memakan ratusan MB dan lambat; dengan virtual thread, murah dan cepat. Yang paling menarik: kode ditulis sekuensial biasa — tidak ada callback/async state machine yang rumit. Ini "concurrency tanpa mengorbankan keterbacaan".
Dengan executor biasa, tugas yang berjalan paralel tidak terikat satu sama lain — sulit dibatalkan bersama dan sulit dilaporkan kegagalannya. Structured concurrency memperkenalkan StructuredTaskScope yang membatasi masa hidup child task ke dalam blok — seperti struktur kontrol:
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;
public class Layanan {
public Respon proses() throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<Profil> profil = scope.fork(() -> ambilProfil());
Future<Harga> harga = scope.fork(() -> ambilHarga());
scope.join(); // tunggu semua
scope.throwIfFailed(); // gagal satu = gagal semua
return new Respon(profil.resultNow(), harga.resultNow());
}
}
}fork menjalankan subtask, join menunggu semuanya, dan scope otomatis menutup — subtask yang gagal membatalkan yang lain, dan tidak ada thread yang "bocor" melewati masa hidup scope-nya. Ini membuat concurrency terstruktur: masa hidup dan batas kegagalan jelas, seperti blok kode biasa. Stabil penuh di JDK 25.
Tip
Mulai dari hal yang benar: virtual threads + structured concurrency adalah masa depan Java. Kode berurutan biasa yang sebelumnya "lambat karena satu thread" kini tinggal dijalankan di newVirtualThreadPerTaskExecutor() tanpa mengubah logika — peningkatan tanpa pengorbanan keterbacaan.
Kumpulan aturan yang mencegah sebagian besar bug concurrency:
AtomicInteger/ConcurrentHashMap dari java.util.concurrent lebih aman daripada synchronized manual.Warning
Deadlock adalah bahaya terbesar synchronized: dua thread masing-masing memegang lock yang dibutuhkan yang lain, saling menunggu selamanya. Pencegahannya: selalu akuisisi lock dalam urutan yang sama, dan hindari nested locking bila bisa. Jika program tiba-tiba menggantung tanpa output, curigai deadlock.
Inti yang harus dibawa pulang:
Runnable + Thread.start() untuk paralelisme dasar; output urutannya tidak terjamin.synchronized menjadikannya atomik, volatile memaksa visibilitas memori.Di episode 18 selanjutnya kita akan membahas keamanan & best practice — immutability, defensive copies, avoid null, risiko serialization, secret handling, dan dependency scanning. Sampai jumpa di episode 18!