On this page
Locks & Synchronizers
Beyond synchronized, the java.util.concurrent.locks package provides flexible locking and coordination utilities.
ReentrantLock
A mutual exclusion lock with more features than synchronized:
private final ReentrantLock lock = new ReentrantLock();
public void updateCounter() {
lock.lock();
try {
counter++;
} finally {
lock.unlock(); // always in finally
}
}
Advantages over synchronized
- tryLock() — attempt lock without blocking
- lockInterruptibly() — respond to thread interruption
- Fairness —
new ReentrantLock(true)for FIFO ordering - Multiple Condition objects per lock
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// critical section
} finally {
lock.unlock();
}
} else {
System.out.println("Could not acquire lock");
}
ReadWriteLock
Allows multiple concurrent readers or one exclusive writer:
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private Map<String, String> cache = new HashMap<>();
public String get(String key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
public void put(String key, String value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
Ideal for read-heavy workloads like caches.
Semaphore
Controls access to a limited number of resources:
Semaphore semaphore = new Semaphore(3); // max 3 concurrent accesses
public void accessResource() throws InterruptedException {
semaphore.acquire();
try {
// use limited resource (e.g., database connection)
} finally {
semaphore.release();
}
}
CountDownLatch
One or more threads wait until a set of operations completes:
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
executor.submit(() -> {
try {
doWork();
} finally {
latch.countDown();
}
});
}
latch.await(); // blocks until count reaches zero
System.out.println("All tasks finished");
CyclicBarrier
A set of threads wait for each other at a barrier point, then all proceed:
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All arrived"));
for (int i = 0; i < 3; i++) {
executor.submit(() -> {
doPhase1();
barrier.await(); // wait for all threads
doPhase2();
});
}
Unlike CountDownLatch, a CyclicBarrier is reusable.
StampedLock (Java 8+)
Optimistic read lock for read-heavy scenarios:
StampedLock sl = new StampedLock();
double x, y;
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead();
double curX = x, curY = y;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
curX = x;
curY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(curX * curX + curY * curY);
}
When to Use What
| Tool | Use Case |
|---|---|
synchronized |
Simple mutual exclusion |
ReentrantLock |
Need tryLock, fairness, or conditions |
ReadWriteLock |
Many readers, few writers |
Semaphore |
Limit concurrent access to N resources |
CountDownLatch |
Wait for N events to complete |
CyclicBarrier |
Threads synchronize at a checkpoint |
Best Practices
- Always unlock in a
finallyblock - Prefer higher-level utilities (
ConcurrentHashMap) over manual locking when possible - Avoid holding locks during I/O operations
- Use
ReadWriteLockonly when reads significantly outnumber writes — it has overhead