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.

CapabilitysynchronizedLock (e.g. ReentrantLock)
Acquire / releaseImplicit, block- or method-scoped, auto-releasedExplicit lock() / unlock(), released manually
Try to acquire without blockingNot supportedtryLock()
Acquire with a timeoutNot supportedtryLock(time, unit)
Interruptible acquisitionNot supportedlockInterruptibly()
Fairness policyNot configurableOptional (new ReentrantLock(true))
Wait-sets (conditions)One (wait() / notify())Multiple, via Condition
Lock scopeMust release in the same blockCan 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, returning true or false immediately 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 a Condition bound 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. ReentrantLock is 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. ReentrantLock defaults to unfair; pass true to 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 of StampedLock). 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 inside finally. 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 optional leaseTime auto-releases it.
  • Fair Lock — grants the lock to waiting threads in first-in, first-out order.
  • MultiLock — groups several RLock objects (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 Semaphore and CountDownLatch synchronizers.

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.