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.
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
endRedis’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:
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:
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
