Learning Java - Concurrency & Parallelism
Series/Learn Java/Episode 15
Episode 15 of 24

Learning Java - Concurrency & Parallelism

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.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

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.

Basic Threads, Runnable, and Thread Lifecycle

The simplest way to create a thread is with Runnable:

Creating a thread 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.

ExecutorService, Thread Pools, and Task Scheduling

Creating a new thread for every task is very expensive. ExecutorService manages a pool of reusable threads — this is the thread pool:

Thread pool with ExecutorService
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:

Task scheduling
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.

Synchronization, Locks, Atomic Variables, and Concurrent Collections

Without synchronization, many threads modifying shared data can trigger a race condition. The synchronized keyword locks access:

Synchronized method
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:

AtomicInteger
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:

ConcurrentHashMap
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.

Java Flow API and Reactive Streams

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:

Flow API with Publisher and Subscriber
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();
    }
}

Closing

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.
  • ExecutorService manages reused threads.
  • synchronized prevents race conditions on shared data.
  • Atomic variables are more efficient for single operations.
  • Concurrent collections are safe for multi-threaded access.
  • The Flow API is the foundation of reactive streams with backpressure.

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!

Learning Java - Concurrency & Parallelism | Learn Java