thecodex.expert · The Codex Family of Knowledge
Java

Concurrency in Java in Java

True multi-threading on multiple cores — powerful and worth understanding correctly.

Java 21 LTSOpenJDKLast verified:
Canonical Definition

Java concurrency uses OS threads sharing heap memory — synchronized blocks and methods provide mutual exclusion, volatile ensures cross-thread visibility, ExecutorService manages thread pools, and Java 21 virtual threads (Project Loom) enable millions of concurrent tasks with blocking code.

One sentence

Java has true multi-threading — multiple threads share heap memory and run simultaneously on multiple CPU cores, which is powerful for performance but requires careful synchronisation to avoid data races.

Threads and Runnable

Javathreads.java
// Create a thread — extend Thread or implement Runnable
// Runnable is preferred (doesn't tie you to Thread hierarchy)
class Counter implements Runnable {
    private int count = 0;

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) count++;
    }
    int getCount() { return count; }
}

// Lambda shorthand for Runnable
Thread t1 = new Thread(() -> System.out.println("Hello from thread " + Thread.currentThread().getId()));
t1.start();   // start the thread
t1.join();    // wait for it to finish

// Thread lifecycle: NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED

Synchronisation

Javasynchronization.java
// The problem: two threads incrementing without synchronisation
// produces wrong results (data race)
class UnsafeCounter {
    private int count = 0;
    void increment() { count++; }   // not atomic: read, add, write
    int get() { return count; }
}

// Fix 1: synchronized method — one thread at a time
class SafeCounter {
    private int count = 0;
    synchronized void increment() { count++; }
    synchronized int get() { return count; }
}

// Fix 2: AtomicInteger — lock-free, faster for simple counters
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet();   // atomic read-modify-write

// Fix 3: ReentrantLock — more control than synchronized
import java.util.concurrent.locks.ReentrantLock;
ReentrantLock lock = new ReentrantLock();
try {
    lock.lock();
    count++;
} finally {
    lock.unlock();   // always unlock in finally
}

// volatile — ensures visibility across threads (not atomicity)
volatile boolean running = true;
// Without volatile, a thread might cache running=true and never see the change

ExecutorService and thread pools

Javaexecutors.java
import java.util.concurrent.*;

// Thread pools — reuse threads instead of creating/destroying
ExecutorService pool = Executors.newFixedThreadPool(4);

// Submit tasks
Future<Integer> future = pool.submit(() -> {
    // heavy computation
    return 42;
});

int result = future.get();   // blocks until done
pool.shutdown();             // stop accepting new tasks
pool.awaitTermination(60, TimeUnit.SECONDS);

// Virtual threads (Java 21) — one per task, JVM schedules them
ExecutorService virtual = Executors.newVirtualThreadPerTaskExecutor();
virtual.submit(() -> System.out.println("Virtual thread!"));
virtual.close();  // waits for all tasks

// CompletableFuture — async composition
CompletableFuture<String> cf = CompletableFuture
    .supplyAsync(() -> fetchUser(1))       // run async
    .thenApply(user -> user.getName())      // transform result
    .thenCombine(
        CompletableFuture.supplyAsync(() -> fetchScore(1)),
        (name, score) -> name + ": " + score
    )
    .exceptionally(e -> "Error: " + e.getMessage());

String value = cf.join();   // like get() but unchecked

Concurrent collections

Javaconcurrent_collections.java
import java.util.concurrent.*;

// ConcurrentHashMap — thread-safe, much better than Collections.synchronizedMap
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.computeIfAbsent("b", k -> k.length());   // atomic read-if-absent-compute

// CopyOnWriteArrayList — thread-safe list for read-heavy workloads
// Writes create a new copy of the array — expensive but lock-free reads
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

// BlockingQueue — producer/consumer pattern
BlockingQueue<String> queue = new LinkedBlockingQueue<>(100);

// Producer
new Thread(() -> {
    try {
        queue.put("task1");   // blocks if queue full
    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();

// Consumer
new Thread(() -> {
    try {
        String task = queue.take();   // blocks if queue empty
        process(task);
    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();

The happens-before relation

The Java Memory Model defines happens-before rules. Key rules: (1) program order — each action in a thread happens-before every subsequent action in the same thread; (2) monitor lock — unlocking a monitor happens-before every subsequent lock of the same monitor; (3) volatile write — a write to a volatile variable happens-before every subsequent read of that variable; (4) thread start — Thread.start() happens-before any action in the started thread; (5) thread join — all actions in a thread happen-before Thread.join() returns. Without a happens-before chain between threads, there are no visibility guarantees — a thread may see stale values.

Commonly confused
synchronized guarantees both mutual exclusion AND visibility. volatile only guarantees visibility — a volatile read always sees the latest write, but volatile does not prevent check-then-act race conditions. Use synchronized or AtomicXxx when you need both.
Virtual threads (Java 21) are not goroutines or async/await. Virtual threads are still threads — you write blocking code normally. The JVM handles the scheduling. You cannot await a virtual thread; you use the same thread API. The benefit is that millions can exist without exhausting OS threads.
Thread.sleep() does not release locks. If a thread holds a synchronized lock and calls Thread.sleep(), it sleeps while holding the lock — other threads cannot enter the synchronized block. Use Object.wait() inside a synchronized block to sleep and release the lock simultaneously.
How this connects
Requires first

The Java Memory Model formally

JLS §17 specifies the JMM as a partial order on memory operations. The JMM is intentionally weak — it allows reordering for performance — but guarantees safety for correctly synchronised programs ("data race free" programs behave as sequentially consistent). The JMM was redesigned in Java 5 (JSR-133) to fix problems with the original Java 1.0 model. Key insight: the JMM does not prohibit reordering; it defines when reorderings are observable. Hardware memory models (x86 TSO, ARM, Power) have their own reordering rules, and the JVM must emit the appropriate memory barriers (MFENCE, DMB) to implement the JMM guarantees on each architecture.

Sources

1
Java Language Specification §17 — Threads and Locks. docs.oracle.com/javase/specs/jls/se21/html/jls-17.html.
2
JEP 444 — Virtual Threads (Java 21). openjdk.org/jeps/444.
3
Java SE 21 API — java.util.concurrent. docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/package-summary.html.
4
Goetz, B. et al. (2006). Java Concurrency in Practice. Addison-Wesley.
Source confidence: HighLast verified: Primary source: Java SE Documentation · docs.oracle.com/en/java/

Sources

1
JLS §17 — Threads and Locks. docs.oracle.com/javase/specs/jls/se21/html/jls-17.html.
2
JEP 444 — Virtual Threads. openjdk.org/jeps/444.
3
Java SE 21 — java.util.concurrent. docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/package-summary.html.
4
Goetz, B. et al. (2006). Java Concurrency in Practice. Addison-Wesley.