This episode covers concurrency in Java: basic threads, Runnable, and thread lifecycle, ExecutorService, thread pools, and task scheduling, synchronization, locks, atomic variables, and concurrent collections, plus the Java Flow API, reactive streams, and the java.util.concurrent structure.

Modern CPUs have many cores, and an application that uses a single thread wastes that capability. Episode 15 covers concurrency and parallelism in Java — how to run many tasks simultaneously, safely and efficiently.
You will learn basic threads and their lifecycle, ExecutorService and thread pools, synchronization, atomic variables, concurrent collections, and reactive streams.
The simplest way to create a thread is with Runnable:
public class ThreadDasar {
public static void main(String[] args) {
Runnable tugas = () -> System.out.println("Jalan di thread: "
+ Thread.currentThread().getName());
Thread t = new Thread(tugas);
t.start();
System.out.println("Thread utama selesai");
}
}new Thread(tugas).start() starts a thread asynchronously. Threads have a lifecycle: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — understanding it helps debug concurrency.
Creating a new thread for every task is very expensive. ExecutorService manages a pool of reusable threads — this is the thread pool:
import java.util.concurrent.*;
public class PoolExecutor {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
final int nomor = i;
executor.submit(() ->
System.out.println("Tugas " + nomor + " di thread "
+ Thread.currentThread().getName()));
}
executor.shutdown();
}
}Executors.newFixedThreadPool(4) creates a pool with 4 threads shared alternately across 10 tasks.
For scheduled tasks, use ScheduledExecutorService:
import java.util.concurrent.*;
public class Jadwal {
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() ->
System.out.println("Heartbeat"), 0, 2, TimeUnit.SECONDS);
}
}scheduleAtFixedRate runs a task every 2 seconds — useful for periodic jobs.
Without synchronization, many threads modifying shared data can trigger a race condition. The synchronized keyword locks access:
public class Counter {
private int nilai = 0;
public synchronized void increment() {
nilai++;
}
public int getNilai() {
return nilai;
}
}public synchronized void increment() ensures only one thread increments the value at a time.
For simple operations, an atomic variable is more efficient than synchronized:
import java.util.concurrent.atomic.AtomicInteger;
public class Atomik {
public static void main(String[] args) {
AtomicInteger nilai = new AtomicInteger(0);
nilai.incrementAndGet();
System.out.println(nilai.get());
}
}incrementAndGet() increments the value atomically without a lock. For shared data, the java.util.concurrent package provides thread-safe collections:
import java.util.concurrent.ConcurrentHashMap;
public class KoleksiAman {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> stok = new ConcurrentHashMap<>();
stok.put("Laptop", 10);
stok.merge("Laptop", 1, Integer::sum);
System.out.println(stok.get("Laptop"));
}
}ConcurrentHashMap.merge is safe to call from many threads at once.
Reactive streams handle asynchronous data processing with backpressure — consumers control the rate of data production. Java 9 introduced the Flow API with the Publisher, Subscriber, Subscription, and Processor interfaces:
import java.util.concurrent.Flow.*;
public class DemoFlow {
public static void main(String[] args) {
SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
Subscriber<String> subscriber = new Subscriber<>() {
private Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(String item) {
System.out.println("Menerima: " + item);
subscription.request(1);
}
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Selesai");
}
};
publisher.subscribe(subscriber);
publisher.submit("data pertama");
publisher.close();
}
}Episode 15 covers concurrency: basic threads and lifecycle, ExecutorService with thread pools and scheduling, synchronization and locks, atomic variables, concurrent collections, and the Java Flow API and reactive streams.
Key takeaways:
new Thread(...).start() for simple threads; thread pools for scale.synchronized prevents race conditions on shared data.In the next episode, episode 16, we will discuss performance tuning and garbage collection — JVM profiling with JVisualVM, JFR, and Flight Recorder, understanding the G1, ZGC, and Shenandoah garbage collectors, heap tuning and JVM flags, measuring latency versus throughput, and code optimization techniques. Time to make the application fast!