JavaConcurrency Utilities

Concurrency Utilities

Beyond executors and futures, java.util.concurrent (and its java.util.concurrent.atomic sub-package) provides a whole toolbox of building blocks for writing correct multi-threaded code without hand-rolling synchronized blocks or manual wait()/notify() logic. This page tours the ones you will reach for most often.

ConcurrentHashMap

A plain HashMap is not thread-safe — concurrent reads and writes from multiple threads can corrupt its internal structure or produce wrong results. The traditional fix was to wrap every access in synchronized, but that serializes all access to the map, even reads that don't conflict with each other. ConcurrentHashMap is a drop-in Map replacement designed for exactly this situation: it allows many threads to read and write concurrently with far less contention, because it only locks small internal segments instead of the whole map.

ConcurrentHashMap used from multiple threads

Java
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Map<String, Integer> visits = new ConcurrentHashMap<>();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                visits.merge("home", 1, Integer::sum);
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(visits.get("home")); // reliably 2000
    }
}
CountDownLatch

A CountDownLatch lets one or more threads wait until a set number of operations happening on other threads have completed. You initialize it with a count; each finished operation calls .countDown(), and any thread waiting on .await() is released once the count reaches zero.

Waiting for several workers to finish

Java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        int workerCount = 3;
        CountDownLatch latch = new CountDownLatch(workerCount);
        ExecutorService pool = Executors.newFixedThreadPool(workerCount);

        for (int i = 1; i <= workerCount; i++) {
            int id = i;
            pool.submit(() -> {
                System.out.println("Worker " + id + " finished setup");
                latch.countDown();
            });
        }

        latch.await(); // blocks here until all 3 have counted down
        System.out.println("All workers ready — starting main phase");
        pool.shutdown();
    }
}
Semaphore

A Semaphore limits how many threads can access a resource at once. It holds a fixed number of “permits”: a thread calls .acquire() to take a permit (blocking if none are available) and .release() to give it back. This is the standard tool for capping concurrent access to something like a limited connection pool.

Limiting concurrent access with a Semaphore

Java
import java.util.concurrent.Semaphore;

public class Main {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2); // only 2 threads at a time

        Runnable task = () -> {
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName() + " is working");
                Thread.sleep(300);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                semaphore.release();
            }
        };

        for (int i = 0; i < 5; i++) {
            new Thread(task).start();
        }
    }
}
Atomic counters

Incrementing a plain int shared between threads (count++) is not atomic — it is really a read, an increment, and a write, and two threads can interleave those steps and lose an update. The classic fix is synchronized, but that means acquiring a lock for every single increment. AtomicInteger and AtomicLong instead use lock-free compare-and-swap (CAS) operations at the hardware level, giving you a thread-safe counter without ever blocking a thread on a lock.

AtomicInteger vs. an unsafe plain int

Java
import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger counter = new AtomicInteger(0);

        Runnable incrementTask = () -> {
            for (int i = 0; i < 100_000; i++) {
                counter.incrementAndGet(); // atomic, lock-free
            }
        };

        Thread t1 = new Thread(incrementTask);
        Thread t2 = new Thread(incrementTask);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(counter.get()); // reliably 200000

        // A plain "int count" incremented the same way from two threads
        // would very likely print something less than 200000, because
        // "count++" is not a single atomic operation.
    }
}
You rarely need to write synchronized yourself
The `java.util.concurrent` package exists precisely so that application code almost never has to write raw `synchronized` blocks or manual `wait()`/`notify()` logic by hand. Between `ConcurrentHashMap`, `CountDownLatch`, `Semaphore`, the atomic classes, and executors, most everyday concurrency problems have a purpose-built, well-tested tool already available.
  • ConcurrentHashMap is a thread-safe Map with far less lock contention than manually synchronizing a HashMap.

  • CountDownLatch lets a thread wait until a fixed number of operations on other threads have completed.

  • Semaphore caps how many threads can concurrently access a resource, via .acquire()/.release() permits.

  • AtomicInteger/AtomicLong provide lock-free, thread-safe counters using compare-and-swap instead of synchronized.

  • These utilities mean most application code never needs to hand-write low-level locking logic.