Use synchronized blocks, ReentrantLock, ReadWriteLock, and StampedLock to protect shared state.
Published February 12, 2025
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 |
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.