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
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
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
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
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.
}
}ConcurrentHashMapis a thread-safeMapwith far less lock contention than manually synchronizing aHashMap.CountDownLatchlets a thread wait until a fixed number of operations on other threads have completed.Semaphorecaps how many threads can concurrently access a resource, via.acquire()/.release()permits.AtomicInteger/AtomicLongprovide lock-free, thread-safe counters using compare-and-swap instead ofsynchronized.These utilities mean most application code never needs to hand-write low-level locking logic.