Prevent race conditions with synchronized blocks, ReentrantLock, and ReadWriteLock.
Published September 21, 2026
When multiple threads access shared mutable state, you need mutual exclusion — only one thread should modify the state at a time. Java offers two mechanisms: the synchronized keyword and the java.util.concurrent.locks package.
public class Counter {
private int count = 0;
// Method-level lock (locks on 'this')
public synchronized void increment() {
count++;
}
// Block-level lock (preferred — smaller critical section)
public void incrementBlock() {
synchronized (this) {
count++;
}
}
// Static synchronized — locks on the Class object
public static synchronized void staticMethod() { ... }
}
Every Java object has an intrinsic lock (monitor). synchronized acquires this lock on entry and releases it on exit — even if an exception is thrown.
// Two synchronized methods on the same object share the same lock
public class BankAccount {
private double balance;
public synchronized void deposit(double amount) { balance += amount; }
public synchronized void withdraw(double amount) { balance -= amount; }
// deposit and withdraw cannot run concurrently on the same BankAccount
}
import java.util.concurrent.locks.*;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // ALWAYS unlock in finally!
}
}
// Try to acquire lock without blocking
public boolean tryIncrement() {
if (lock.tryLock()) {
try { count++; return true; }
finally { lock.unlock(); }
}
return false;
}
// Try with timeout
public boolean tryIncrementTimeout() throws InterruptedException {
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try { count++; return true; }
finally { lock.unlock(); }
}
return false;
}
}
public class Cache<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
public V get(K key) {
readLock.lock(); // multiple threads can read simultaneously
try { return map.get(key); }
finally { readLock.unlock(); }
}
public void put(K key, V value) {
writeLock.lock(); // exclusive write access
try { map.put(key, value); }
finally { writeLock.unlock(); }
}
}
public class BoundedQueue<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) notFull.await();
queue.add(item);
notEmpty.signal();
} finally { lock.unlock(); }
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await();
T item = queue.poll();
notFull.signal();
return item;
} finally { lock.unlock(); }
}
}
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Auto-unlock on exception | ✅ | ❌ (need finally) |
| Fairness policy | No | Yes (new ReentrantLock(true)) |
| tryLock() | No | ✅ |
| Multiple conditions | No | ✅ |
| Code readability | Better | More verbose |
ReentrantReadWriteLock lets multiple readers proceed concurrently, but every reader still has to acquire an actual read lock — cheap, but not free, especially under very high read contention. StampedLock (Java 8+) adds a third mode, optimistic reading, that avoids acquiring a lock at all for the common case:
public class Point {
private final StampedLock lock = new StampedLock();
private double x, y;
double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead(); // no actual lock taken
double currentX = x, currentY = y; // read without blocking anyone
if (!lock.validate(stamp)) { // did a writer sneak in while we read?
stamp = lock.readLock(); // fall back to a real read lock
try { currentX = x; currentY = y; }
finally { lock.unlockRead(stamp); }
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
void move(double deltaX, double deltaY) {
long stamp = lock.writeLock();
try { x += deltaX; y += deltaY; }
finally { lock.unlockWrite(stamp); }
}
}
tryOptimisticRead() doesn't block writers at all — it just hands back a stamp. The read proceeds speculatively; validate(stamp) afterward checks whether a write happened in the meantime. If not, the optimistic read was safe and free of any locking cost; if a writer did intervene, the code falls back to a real (blocking) read lock. This trades a small amount of complexity for significantly better throughput under read-heavy, write-rare contention — the exact profile ReentrantReadWriteLock already targets, just faster in the common case.
synchronized is reentrant — a thread holding the lock can re-enter synchronized methods on the same object without blocking.volatile is NOT a replacement for synchronized — it only guarantees visibility, not atomicity of compound operations.