java.util.concurrent replaces most hand-written synchronized/wait code with tested building blocks: concurrent collections, atomic variables, explicit locks and synchronizers.
Thread-safe maps compared
Hashtable | Collections.synchronizedMap | ConcurrentHashMap | |
|---|---|---|---|
| Locking | Every method locks the whole table | Every method locks one mutex | CAS + per-bin locks; reads are lock-free |
| Concurrent reads/writes | No | No | Yes |
null keys/values | Not allowed | Allowed (if the backing map allows) | Not allowed |
| Iteration while modifying | Fail-fast | Must lock manually; else CME | Weakly consistent, never throws CME |
| Atomic compound ops | No | No | merge, compute, putIfAbsent… |
| Status | Legacy | Simple wrapper | Default choice |
import java.util.List;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
public class Main {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
List<String> words = List.of("a", "b", "a", "c", "a", "b");
Runnable countAll = () -> { for (String w : words) counts.merge(w, 1, Integer::sum); }; // atomic
Thread[] ts = new Thread[4];
for (int i = 0; i < ts.length; i++) (ts[i] = new Thread(countAll)).start();
for (Thread t : ts) t.join();
System.out.println(new TreeMap<>(counts)); // sorted for display
counts.computeIfAbsent("d", k -> 0);
counts.compute("d", (k, v) -> v + 100);
System.out.println("d = " + counts.get("d") + ", size = " + counts.size());
}
}{a=12, b=8, c=4}
d = 100, size = 4Atomic variables and CAS
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
public class Main {
public static void main(String[] args) throws InterruptedException {
AtomicInteger a = new AtomicInteger(5);
System.out.println(a.compareAndSet(5, 10) + " " + a.get()); // expected 5: swap
System.out.println(a.compareAndSet(5, 20) + " " + a.get()); // stale expectation: no swap
System.out.println(a.updateAndGet(x -> x * 3));
System.out.println(a.getAndIncrement() + " then " + a.get());
AtomicInteger hits = new AtomicInteger();
LongAdder adder = new LongAdder();
Runnable work = () -> {
for (int i = 0; i < 10_000; i++) { hits.incrementAndGet(); adder.increment(); }
};
Thread[] ts = new Thread[4];
for (int i = 0; i < ts.length; i++) (ts[i] = new Thread(work)).start();
for (Thread t : ts) t.join();
System.out.println("AtomicInteger = " + hits.get() + ", LongAdder = " + adder.sum());
}
}true 10
false 10
30
30 then 31
AtomicInteger = 40000, LongAdder = 40000| Class | Use |
|---|---|
AtomicInteger / AtomicLong | Counters, sequence numbers, compareAndSet state machines |
AtomicBoolean | Run-once flags (if (started.compareAndSet(false, true))) |
AtomicReference<V> | Swap an immutable object atomically |
LongAdder | Hot counters under heavy contention — cells per thread, summed on read |
AtomicIntegerArray | Per-index atomic updates |
ReentrantLock and Condition
An explicit Lock does what synchronized does plus: try without blocking, time out, be interrupted while waiting, optional fairness, and several wait-sets (Conditions) per lock.
import java.util.concurrent.locks.ReentrantLock;
public class Main {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
lock.lock(); // main holds it
try {
Thread t = new Thread(() -> System.out.println("worker tryLock: " + lock.tryLock()));
t.start();
t.join();
} finally {
lock.unlock(); // ALWAYS unlock in finally
}
Thread t2 = new Thread(() -> {
if (lock.tryLock()) {
try { System.out.println("worker tryLock after unlock: true"); }
finally { lock.unlock(); }
}
});
t2.start();
t2.join();
System.out.println("held now? " + lock.isLocked());
}
}worker tryLock: false
worker tryLock after unlock: true
held now? falseimport java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class LockBuffer<T> {
private final Deque<T> items = new ArrayDeque<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // two separate wait-sets
private final Condition notEmpty = lock.newCondition();
LockBuffer(int capacity) { this.capacity = capacity; }
void put(T x) throws InterruptedException {
lock.lock();
try {
while (items.size() == capacity) notFull.await();
items.addLast(x);
notEmpty.signal(); // wake only a consumer
} finally {
lock.unlock();
}
}
T take() throws InterruptedException {
lock.lock();
try {
while (items.isEmpty()) notEmpty.await();
T x = items.removeFirst();
notFull.signal(); // wake only a producer
return x;
} finally {
lock.unlock();
}
}
}synchronized | ReentrantLock | |
|---|---|---|
| Release | Automatic at block end | Manual unlock() in finally |
| Try / timeout | No | tryLock(), tryLock(t, unit) |
| Interruptible wait for lock | No | lockInterruptibly() |
| Fairness | No | new ReentrantLock(true) (FIFO-ish, slower) |
| Conditions | One wait-set (wait/notify) | Many (newCondition()) |
| Reentrant | Yes | Yes |
ReentrantReadWriteLock lets many readers in at once but writers alone — good for read-mostly data. StampedLock (Java 8) adds optimistic reads for even cheaper read paths.
Synchronizers
| Tool | What it does | Reusable? | Typical use |
|---|---|---|---|
CountDownLatch(n) | await() blocks until countDown() was called n times | No | Wait for N services to start / N tasks to finish |
CyclicBarrier(n, action) | n threads wait for each other, then all continue | Yes | Phased parallel algorithms |
Semaphore(permits) | acquire()/release() permits | Yes | Limit concurrency (max 10 DB connections) |
Phaser | Flexible barrier with dynamic parties | Yes | Multi-phase work with join/leave |
Exchanger | Two threads swap objects | Yes | Pipeline buffers |
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) throws InterruptedException {
// CountDownLatch: wait for 3 workers
CountDownLatch done = new CountDownLatch(3);
List<String> results = new CopyOnWriteArrayList<>();
for (String svc : List.of("db", "cache", "queue")) {
new Thread(() -> { results.add(svc + " ready"); done.countDown(); }).start();
}
done.await();
System.out.println(results.stream().sorted().toList());
// Semaphore: at most 2 tasks inside at once
Semaphore permits = new Semaphore(2);
AtomicInteger inside = new AtomicInteger(), maxInside = new AtomicInteger();
try (ExecutorService pool = Executors.newFixedThreadPool(6)) {
for (int i = 0; i < 6; i++) {
pool.submit(() -> {
permits.acquire();
try {
maxInside.accumulateAndGet(inside.incrementAndGet(), Math::max);
Thread.sleep(20);
inside.decrementAndGet();
} finally {
permits.release();
}
return null;
});
}
}
System.out.println("max concurrent <= 2: " + (maxInside.get() <= 2));
}
}[cache ready, db ready, queue ready]
max concurrent <= 2: trueHow does ConcurrentHashMap achieve thread safety (Java 8+)?
Why doesn't ConcurrentHashMap allow null?
AtomicInteger vs LongAdder?
CountDownLatch vs CyclicBarrier?