Fundamentals 16 min read

Mastering Java Locks: From Pessimistic to Distributed and Optimizations

This article explains the full spectrum of Java locking mechanisms—including pessimistic, optimistic, distributed, reentrant, spin, read/write, fair vs. non‑fair, JVM lock states, and optimization techniques—detailing their principles, use‑cases, SQL/Redis examples, and performance trade‑offs.

dbaplus Community
dbaplus Community
dbaplus Community
Mastering Java Locks: From Pessimistic to Distributed and Optimizations

Pessimistic Lock

In pessimistic locking the system assumes concurrent updates will occur, so it locks the data until the operation completes. Typically implemented via database row locks (e.g., SELECT * FROM table WHERE id = #{id} FOR UPDATE) or Java's synchronized keyword for single‑node deployments. Row‑level locking requires specifying a primary key; otherwise MySQL may fall back to table locks, severely impacting performance. Auto‑commit must be disabled.

Optimistic Lock

Optimistic locking assumes conflicts are rare; it does not lock during reads but checks a version field at update time. The workflow is: read the record and its version, then execute an update like

UPDATE table SET ..., version = version + 1 WHERE id = #{id} AND version = #{version}

. If the version matches, the update succeeds; otherwise the operation is retried.

Distributed Lock

When multiple JVMs run on different machines, single‑node locks become ineffective. Distributed locks can be built on databases, Redis, or Zookeeper. A common Redis implementation uses the atomic SET key unique_value NX EX seconds command to acquire a lock and a Lua script to release it safely:

// Compare unique_value before deleting
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

Redis’s high performance makes it popular, but lock loss can occur if the lock holder crashes before replication. Redlock mitigates this by requiring a majority of independent Redis nodes to grant the lock.

Reentrant (Exclusive) Lock

Java’s ReentrantLock and synchronized are exclusive locks: only one thread may hold the lock at a time. They support both fair and non‑fair acquisition policies.

Spin Lock

A spin lock repeatedly checks a condition in a loop until the lock becomes available, consuming CPU cycles but avoiding context switches. It is suitable when contention is low. Example using JUC’s CAS:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

Care must be taken to avoid deadlocks and excessive CPU usage; a timeout or fallback to blocking can be added.

Read/Write Lock

Read/write locks allow concurrent reads while writes remain exclusive. In Java, ReentrantReadWriteLock provides a shared read lock and an exclusive write lock, improving throughput for read‑heavy workloads.

Fair vs. Non‑Fair Lock

Fair locks grant access in request order, preventing starvation but reducing throughput due to queue management overhead. Non‑fair locks let threads “cut in line,” improving performance but risking starvation. The diagram below illustrates the difference:

Fair vs Non‑Fair lock diagram
Fair vs Non‑Fair lock diagram

JVM Lock States (No‑Lock, Biased, Lightweight, Heavyweight)

Since JDK 1.6, the JVM optimizes synchronized by transitioning through four states:

No‑Lock: no synchronization needed (optimistic lock).

Biased Lock: the lock is biased toward the first thread that acquires it, avoiding CAS after the initial acquisition.

Lightweight Lock: uses CAS and spinning when contention is low.

Heavyweight Lock: escalates to OS‑level mutex when contention is high, causing thread blocking.

These states upgrade automatically based on contention and never downgrade.

Lock Optimization Techniques

Lock Coarsening merges multiple adjacent lock acquisitions into a single lock to reduce overhead. Example:

// Before coarsening
for (int i = 0; i < size; i++) {
    synchronized(lock) {
        // business logic
    }
}
// After coarsening
synchronized(lock) {
    for (int i = 0; i < size; i++) {
        // business logic
    }
}

Lock Elimination removes locks when the JVM’s escape analysis proves no other thread can access the synchronized object. For instance, a locally scoped StringBuffer can be replaced by an unsynchronized StringBuilder after compilation, as shown by the generated bytecode diagram:

Lock elimination bytecode example
Lock elimination bytecode example
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Distributed SystemsJVMperformanceconcurrencyLocks
dbaplus Community
Written by

dbaplus Community

Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.