What Is a Java Lock?
A Java lock is a synchronization tool that lets only one thread at a time enter a critical section of code, protecting shared data from concurrent access. In modern Java, "lock" usually refers to the java.util.concurrent.locks.Lock interface and its implementations, such as ReentrantLock, which give you finer-grained control than the built-in synchronized keyword.
Locks exist to prevent race conditions — bugs that occur when two or more threads read and write the same data at the same time, producing results that depend on unpredictable timing. By requiring a thread to acquire a lock before touching shared state and release it afterward, you guarantee mutual exclusion: at most one thread holds the lock, so at most one thread runs the protected code.
Locks vs. the synchronized Keyword
Before the Lock interface arrived in Java 5, the only built-in locking mechanism was the synchronized keyword. synchronized is simple and still useful, but it is rigid: the lock is acquired and released implicitly, tied to a block or method, and always released when that block exits.
The Lock interface makes locking explicit. You call lock() and unlock() yourself, which unlocks (pun intended) capabilities synchronized cannot offer.
| Capability | synchronized | Lock (e.g. ReentrantLock) |
|---|---|---|
| Acquire / release | Implicit, block- or method-scoped, auto-released | Explicit lock() / unlock(), released manually |
| Try to acquire without blocking | Not supported | tryLock() |
| Acquire with a timeout | Not supported | tryLock(time, unit) |
| Interruptible acquisition | Not supported | lockInterruptibly() |
| Fairness policy | Not configurable | Optional (new ReentrantLock(true)) |
| Wait-sets (conditions) | One (wait() / notify()) | Multiple, via Condition |
| Lock scope | Must release in the same block | Can be acquired and released across methods |
The trade-off: because you release a Lock manually, you must do it in a finally block, or a thrown exception can leave the lock held forever.
The Lock Interface
The Lock interface defines Java's standard set of locking operations. Its core methods are:
-
lock()— acquires the lock, waiting indefinitely if another thread holds it. -
tryLock()— acquires the lock only if it is free right now, returningtrueorfalseimmediately instead of blocking. -
tryLock(long time, TimeUnit unit)— waits up to the given time for the lock, then gives up. -
lockInterruptibly()— acquires the lock but allows the waiting thread to be interrupted. -
unlock()— releases the lock. -
newCondition()— returns aConditionbound to this lock for coordinated waiting.
The canonical usage pattern always pairs lock() with a try/finally:
Lock lock = new ReentrantLock();
lock.lock();
try {
// critical section — only one thread runs this at a time
} finally {
lock.unlock(); // always release, even if the body throws
}
To avoid blocking forever, use a timed tryLock:
Lock lock = new ReentrantLock();
// tryLock(time, unit) can throw InterruptedException,
// so the enclosing method must handle or declare it.
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// acquired within one second
} finally {
lock.unlock();
}
} else {
// could not acquire the lock — take an alternative path
}
Types of Locks in Java
Java's java.util.concurrent.locks package ships several lock implementations, and the concurrency literature describes several lock behaviors. It helps to separate the concrete classes from the conceptual categories.
Concrete Implementations
ReentrantLock is the general-purpose mutual-exclusion lock and the most common Lock implementation. "Reentrant" means the thread that holds the lock can acquire it again without deadlocking itself; the lock keeps a hold count and is only released when unlock() has been called as many times as lock().
Lock lock = new ReentrantLock();
ReadWriteLock (implemented by ReentrantReadWriteLock) splits a single lock into two: a read lock that any number of threads can hold simultaneously, and a write lock that only one thread can hold and only when no readers are active. This dramatically improves throughput for data that is read often and written rarely.
ReadWriteLock rwLock = new ReentrantReadWriteLock();
// Many readers can hold this at once
rwLock.readLock().lock();
try {
// read shared state
} finally {
rwLock.readLock().unlock();
}
// Only one writer, and only when there are no readers
rwLock.writeLock().lock();
try {
// mutate shared state
} finally {
rwLock.writeLock().unlock();
}
StampedLock (added in Java 8) is a more advanced read/write lock that also supports optimistic reads. Instead of blocking, a reader takes a cheap "stamp," reads the data, and then validates the stamp; if a writer intervened, it retries with a full read lock. StampedLock is not reentrant and does not implement the Lock interface directly, but it is powerful for read-heavy, low-contention workloads.
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead();
// read fields into local variables here
if (!lock.validate(stamp)) {
// a write happened; fall back to a blocking read lock
stamp = lock.readLock();
try {
// re-read fields
} finally {
lock.unlockRead(stamp);
}
}
Conceptual Categories
-
Reentrant vs. non-reentrant — whether the holding thread can re-acquire the same lock.
ReentrantLockis reentrant; a naive spin lock often is not. -
Fair vs. unfair — a fair lock grants access in the order threads requested it (FIFO), preventing starvation at some throughput cost.
ReentrantLockdefaults to unfair; passtrueto make it fair:Lock fairLock = new ReentrantLock(true); // first-in, first-out ordering -
Pessimistic vs. optimistic — a pessimistic lock blocks other threads up front (
ReentrantLock, the write path ofStampedLock). An optimistic lock assumes no conflict, proceeds without blocking, and only retries if a conflict is detected (StampedLock.tryOptimisticRead()). - Spin lock — a lock where a waiting thread repeatedly checks (spins) instead of sleeping. Spinning avoids context-switch overhead for very short critical sections but wastes CPU if held long.
Coordinating Threads With Condition
A Lock can produce one or more Condition objects, which replace the single wait-set of Object.wait() / notify(). Conditions let a thread release the lock and wait until another thread signals that some state has changed — for example, a consumer waiting until a queue is non-empty. Because a lock can hand out multiple conditions, you can wait on distinct predicates (say, "not full" and "not empty") independently, which is not possible with synchronized.
Common Pitfalls
-
Forgetting to
unlock()— always release insidefinally. A lock leaked on an exception path stalls every other thread. - Deadlock — two threads each holding a lock the other needs. Acquire multiple locks in a consistent global order to avoid it.
- Holding a lock too long — keep critical sections short; do slow work (I/O, network calls) outside the lock.
- Locking on the wrong object — the threads you want to coordinate must contend for the same lock instance.
From Single-JVM Locks to Distributed Locks
Every lock discussed so far lives inside one JVM. ReentrantLock and synchronized coordinate threads in a single process — they know nothing about a second application instance running on another server. The moment you scale horizontally, in-process locks can no longer guarantee that only one worker touches a shared resource.
Coordinating exclusive access across processes and machines requires a distributed lock — a lock whose state lives in a shared external system that every instance can see, such as Valkey or Redis. This is exactly the problem Redisson solves with Redis Lock.
The convenient part: Redisson's distributed lock, RLock, extends the standard java.util.concurrent.locks.Lock interface. If you already know the Java Lock interface, you already know the API — the same lock() / unlock() pattern now works across your entire cluster.
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// critical section — now coordinated across every JVM
} finally {
lock.unlock();
}
Redisson Lock and Synchronizer Types
Redisson extends the familiar locking model with a suite of distributed locks and synchronizers:
-
RLock— a reentrant distributed lock. A built-in watchdog automatically extends the lock's lease while the owner is alive (default 30 seconds), so a crashed client can't hold the lock forever, and an optionalleaseTimeauto-releases it. - Fair Lock — grants the lock to waiting threads in first-in, first-out order.
-
MultiLock — groups several
RLockobjects (even across different Redisson instances) and treats them as one lock. - ReadWriteLock — a distributed version of the read/write pattern above.
- Spin Lock — uses exponential backoff instead of pub/sub, ideal when many locks are acquired and released in a short interval.
- Fenced Lock — issues fencing tokens to protect against a client that pauses and later resumes believing it still holds the lock.
-
Semaphore, PermitExpirableSemaphore, and CountDownLatch — distributed versions of Java's
SemaphoreandCountDownLatchsynchronizers.
Java Lock: Frequently Asked Questions
Is synchronized a Lock?
Yes. synchronized acquires an intrinsic (monitor) lock on an object implicitly. The Lock interface offers the same mutual exclusion but with explicit control, timeouts, interruptibility, and fairness.
What Is the Difference Between Lock and ReentrantLock?
Lock is the interface that defines locking operations; ReentrantLock is the most common class that implements it. In practice you declare a variable of type Lock and instantiate it as a ReentrantLock.
When Should I Use a Lock Instead of synchronized?
Use a Lock when you need a non-blocking tryLock(), a timeout, interruptible waiting, fairness, or multiple wait conditions. Use synchronized for simple, short critical sections where its automatic release is an advantage.
What Is a Reentrant Lock?
A reentrant lock lets the thread that already holds it acquire it again without blocking. The lock tracks a hold count and is fully released only after unlock() is called once per lock().
How Do I Lock Across Multiple JVMs or Servers?
An in-process lock like ReentrantLock cannot coordinate separate processes. Use a distributed lock backed by a shared store — for example, Redisson's RLock, which extends the Java Lock interface and works across your whole cluster.