Java already had synchronized, so the Lock interface was not added to replace locking in general.
It was added because some coordination problems need more control than intrinsic monitors expose cleanly.
If your code needs only one critical section and straightforward mutual exclusion, synchronized is still a strong default.
If your code needs timing, cancellation, multiple wait queues, or richer acquisition behavior, Lock becomes useful.
Problem Statement
Consider a backend service that refreshes tenant routing tables.
Only one refresh should run at a time, but the caller also wants these rules:
- request threads should not block forever waiting for a refresh lock
- shutdown should be able to cancel blocked maintenance work
- different waiters may need different wake-up conditions
That combination is awkward with plain synchronized.
Why synchronized Was Not Enough
Intrinsic locking gives you:
- mutual exclusion
- visibility on monitor entry and exit
- monitor-based wait/notify coordination
What it does not give directly:
- non-blocking acquisition
- timed acquisition
- interruptible acquisition while waiting for the lock
- multiple explicit condition queues per lock
Those gaps are exactly why java.util.concurrent.locks exists.
Basic Example
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class InventoryService {
private final Lock lock = new ReentrantLock();
private int available = 10;
boolean reserveOne() {
lock.lock();
try {
if (available > 0) {
available--;
return true;
}
return false;
} finally {
lock.unlock();
}
}
}
This looks similar to synchronized, which is fine.
The value is not the syntax.
The value is that the same lock object can later support tryLock, lockInterruptibly, and Condition.
Runnable Example
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockInterfaceDemo {
public static void main(String[] args) throws Exception {
TenantRefreshCoordinator coordinator = new TenantRefreshCoordinator();
Thread refreshThread = new Thread(() -> {
coordinator.refreshRoutingTable();
}, "refresh-worker");
refreshThread.start();
TimeUnit.MILLISECONDS.sleep(50);
boolean fastPath = coordinator.tryReadConfigForRequest();
System.out.println("Request got immediate access = " + fastPath);
refreshThread.join();
}
static final class TenantRefreshCoordinator {
private final Lock lock = new ReentrantLock();
private String routingVersion = "v1";
void refreshRoutingTable() {
lock.lock();
try {
sleep(300);
routingVersion = "v2";
System.out.println("Refreshed routing to " + routingVersion);
} finally {
lock.unlock();
}
}
boolean tryReadConfigForRequest() {
if (!lock.tryLock()) {
return false;
}
try {
System.out.println("Serving request using " + routingVersion);
return true;
} finally {
lock.unlock();
}
}
}
static void sleep(long millis) {
try {
TimeUnit.MILLISECONDS.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
The example is simple, but it shows the point. One code path takes the lock normally. Another chooses a non-blocking acquisition policy.
That second behavior is the real reason Lock exists.
Production-Style Scenarios
Use Lock when the problem needs one or more of these:
- fail-fast lock acquisition on hot paths
- bounded waiting for administrative tasks
- shutdown-aware blocking that reacts to interruption
- more than one logical wait queue for the same protected state
Common examples:
- bounded resource pools
- refresh or leadership coordination
- schedulers with draining and refill conditions
- shared caches with one refresh path and many waiters
Trade-Offs
Lock is more flexible than synchronized, but it is easier to misuse.
Typical mistakes:
- forgetting
unlock()in afinallyblock - mixing several lock types with no clear discipline
- using explicit locks where plain
synchronizedwould be simpler and safer
The right comparison is not “which one looks modern.” It is “which one matches the coordination problem with the least risk.”
Decision Guide
Prefer synchronized when:
- one monitor boundary is enough
- you do not need timed or interruptible acquisition
- the code should stay as simple as possible
Prefer Lock when:
- waiting policy matters
- interruption during acquisition matters
- multiple conditions matter
- you want an explicit lock object with explicit lifecycle around acquisition
Key Takeaways
Lockexists because some concurrency problems need richer acquisition and coordination behavior than intrinsic monitors provide.- The most important added capabilities are
tryLock, timed acquisition, interruption support, andCondition. synchronizedis still a good default for simple mutual exclusion.- Choose explicit locks when the problem actually needs the extra control.
Next post: ReentrantLock in Java Deep Dive
Comments