Building a custom lock is a good learning exercise because it forces you to think about ownership, waiting, wake-up policy, and failure cases.
It is usually a bad production default.
That tension is the right way to approach this topic: understand the mechanics deeply, then use the JDK’s proven lock implementations for real systems unless you truly need something specialized.
Quick Decision Guide
| Goal | Build a custom lock? |
|---|---|
| learn how wait/notify-based mutual exclusion works | yes |
| ship a normal production mutex | no, use ReentrantLock or synchronized |
| need reentrancy, timeout, conditions, interruption policy | no, use JDK utilities |
| debugging lock ownership and usage patterns | maybe for experiments, not as a default runtime primitive |
The main lesson is not “I can outbuild the JDK.” It is “locking is trickier than it looks.”
Start With the Actual Contract
A lock needs more than “one thread goes in at a time.”
At minimum, a usable mutex contract must answer:
- who owns the lock right now
- what a waiting thread does
- how wake-up works
- what happens if a non-owner unlocks
- whether interruption is respected
Even a tiny teaching lock has concurrency semantics, not just code.
A Small Teaching Implementation
This version is intentionally simple, but it still does two important things:
- uses a
whileloop when waiting - tracks the owning thread
public final class SimpleLock {
private boolean locked;
private Thread owner;
public synchronized void lock() throws InterruptedException {
while (locked) {
wait();
}
locked = true;
owner = Thread.currentThread();
}
public synchronized void unlock() {
if (Thread.currentThread() != owner) {
throw new IllegalMonitorStateException("Current thread does not own the lock");
}
locked = false;
owner = null;
notifyAll();
}
}
Usage:
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
Why while Matters More Than It Looks
Waiting threads must re-check the condition after waking.
That is why this is correct:
while (locked) {
wait();
}
and this is fragile:
if (locked) {
wait();
}
A thread can wake up and still not be allowed to proceed. The guard condition has to be checked again.
That pattern is fundamental to Java monitor-based coordination, not just custom locks.
Why Owner Tracking Matters
Without owner tracking, any thread can call unlock().
That turns a programming mistake into corrupted concurrency semantics.
Owner checks give you two important properties:
- incorrect usage fails fast
- debugging becomes much less mysterious
Concurrency bugs are already expensive. Silent misuse makes them worse.
notify() vs notifyAll()
For a toy lock, notify() sometimes appears to work.
In teaching code, notifyAll() is often the safer choice because it avoids depending on subtle wake-up assumptions.
Why:
- more than one waiter may exist
- future evolution of the lock may add more waiting states
- accidental missed progress is harder to debug than extra wake-ups
notifyAll() is not always the most efficient primitive.
It is often the clearer one for correctness-first code.
What This Lock Still Does Not Handle
Even this improved version is not close to a full production replacement for ReentrantLock.
It still lacks:
- reentrancy
- timed acquisition
- non-blocking
tryLock - condition variables
- rich diagnostics
- carefully tuned fairness behavior
Those are not decorative features. They are the reason production lock implementations are difficult to reproduce correctly.
Reentrancy Sketch
If the same thread should be able to acquire the lock multiple times, you need hold-count tracking:
public final class ReentrantTeachingLock {
private Thread owner;
private int holdCount;
public synchronized void lock() throws InterruptedException {
Thread current = Thread.currentThread();
while (owner != null && owner != current) {
wait();
}
owner = current;
holdCount++;
}
public synchronized void unlock() {
if (Thread.currentThread() != owner) {
throw new IllegalMonitorStateException();
}
holdCount--;
if (holdCount == 0) {
owner = null;
notifyAll();
}
}
}
This is a useful teaching extension. It is still nowhere near the full ergonomics and reliability of JDK locks.
Production Default: Prefer ReentrantLock
If the real goal is “I need a lock in production,” use the JDK implementation:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public final class AccountService {
private final Lock lock = new ReentrantLock();
private int balance;
public void deposit(int amount) {
lock.lock();
try {
balance += amount;
} finally {
lock.unlock();
}
}
}
Use ReentrantLock when you need features like:
- reentrancy
tryLock- interruptible lock acquisition
- optional fairness
Use ReadWriteLock or StampedLock only when the access pattern truly justifies the extra complexity.
Common Misuse Patterns
Holding the lock across slow I/O
Even a correct lock becomes harmful if the critical section includes network calls, file I/O, or long external work.
Forgetting finally
Lock acquisition without guaranteed release is an incident waiting to happen.
Treating custom lock code as harmless infrastructure
A broken lock corrupts everything built on top of it. This is not a good place for casual experimentation in production.
Rebuilding standard concurrency tools for normal use cases
Most teams do not need a novel mutex. They need a correctly used standard one.
Testing a Teaching Lock
If you do build one for learning, test more than the happy path:
- two threads contending for the same lock
- unlock by non-owner thread
- interruption while waiting
- reentrant acquisition if supported
- stress loop to catch missed wake-ups
Concurrency code can look fine in a tiny demo and still fail badly under repetition.
Key Takeaways
- Building a custom lock is valuable for learning, not as a default production strategy.
whileloops, owner tracking, and reliable unlock discipline are essential.- Even a “simple” lock quickly grows into a real concurrency design problem.
- For real systems, prefer the JDK’s lock implementations unless you have a specialized requirement you can justify and test thoroughly.
Categories
Tags
Comments