Optimistic Locking vs Pessimistic Locking in Java: Key Differences and Implementation
Pessimistic locking prevents conflicts by acquiring exclusive locks before accessing shared data using synchronized or ReentrantLock, while optimistic locking assumes contention is rare and uses non-blocking compare-and-swap (CAS) operations or version checks that retry upon collision.
Understanding the difference between optimistic locking and pessimistic locking in Java is essential for building high-performance concurrent applications. The Snailclimb/JavaGuide repository provides comprehensive documentation and runnable examples in docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md that demonstrate how these two strategies protect shared mutable state. While both approaches prevent race conditions, they differ fundamentally in their assumptions about thread contention and their impact on system throughput.
Fundamental Assumptions and Mechanisms
Pessimistic Locking: Conflict is Inevitable
Pessimistic locking operates on the assumption that the resource will be contested. Threads acquire an exclusive lock before reading or writing, forcing other threads to block until the lock is released. This approach uses traditional synchronization mechanisms to serialize access to critical sections.
Optimistic Locking: Conflict is Rare
Optimistic locking assumes the resource won't be contested during most operations. Threads perform reads and writes without acquiring locks, then verify at commit time that the data has not changed. If a conflict is detected, the operation retries until successful.
Implementing Pessimistic Locking in Java
According to the JavaGuide source code, pessimistic strategies rely on the java.util.concurrent.locks package and language-level synchronization to block concurrent access.
Using synchronized Blocks
The simplest form of pessimistic locking uses the synchronized keyword to create a monitor lock that serializes access to critical blocks.
// 1. Pessimistic lock with synchronized
public void performSynchronisedTask() {
synchronized (this) {
// critical section – only one thread at a time
}
}
This approach is implemented in docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md and provides a straightforward mental model where threads block until the monitor becomes available.
Using ReentrantLock for Explicit Control
For more flexible lock acquisition, ReentrantLock allows explicit locking and unlocking with timeout capabilities and interrupt handling.
// 2. Pessimistic lock with ReentrantLock
private final Lock lock = new ReentrantLock();
public void performReentrantLockTask() {
lock.lock();
try {
// critical section
} finally {
lock.unlock(); // always release in finally
}
}
As documented in the repository, this pattern requires careful handling in a finally block to prevent deadlocks during exceptions.
Implementing Optimistic Locking in Java
Optimistic strategies avoid blocking by using atomic operations and version validation, as detailed in the JavaGuide concurrent programming section.
CAS-Based Updates with Atomic Classes
The java.util.concurrent.atomic package provides lock-free classes like LongAdder and AtomicInteger that use Compare-And-Swap (CAS) algorithms. These classes increment counters without blocking threads, making them ideal for high-throughput scenarios.
// 3. Optimistic lock via CAS – LongAdder (high‑throughput counter)
LongAdder counter = new LongAdder(); // no explicit lock
counter.increment(); // CAS‑based update
For manual control over the retry logic, implement a loop using AtomicInteger.compareAndSet():
// 5. Manual CAS loop on an AtomicInteger
AtomicInteger atomic = new AtomicInteger(0);
int expected, update;
do {
expected = atomic.get();
update = expected + 1; // compute new value
} while (!atomic.compareAndSet(expected, update));
Version-Field Strategy for Complex State
For database-style optimistic locking, maintain a version field that increments with each update. The operation only commits if the version remains unchanged since reading.
// 4. Optimistic lock with a version field (DB‑style)
int currentVersion = row.getVersion();
int newBalance = row.getBalance() - 50;
// Try to update only if version unchanged
UPDATE accounts
SET balance = ?, version = version + 1
WHERE id = ? AND version = currentVersion;
This pattern appears in docs/java/concurrent/optimistic-lock-and-pessimistic-lock.md as a solution for banking-balance scenarios where transactions must validate consistency at commit time.
Performance Characteristics and Selection Criteria
Choosing between these strategies depends on your contention profile and consistency requirements.
| Aspect | Pessimistic Locking | Optimistic Locking |
|---|---|---|
| Thread Blocking | Yes—threads wait for lock release | No—threads retry on conflict |
| Deadlock Risk | Possible with incorrect lock ordering | Eliminated (no locks held) |
| Best For | High write ratios, multi-resource atomicity | Read-heavy workloads, single-variable updates |
| CPU Impact | Context-switch overhead during blocking | Busy-waiting during high contention |
Pessimistic locking excels when write contention is high or operations must span multiple resources atomically. Optimistic locking delivers superior performance for read-heavy scenarios with infrequent writes, such as counters and configuration flags.
Summary
- Pessimistic locking acquires exclusive locks before access, using
synchronizedorReentrantLockto prevent conflicts through thread blocking. - Optimistic locking assumes low contention and uses non-blocking CAS operations or version checks, retrying when collisions occur.
- Use pessimistic strategies for high-write workloads requiring strong consistency across multiple resources.
- Use optimistic strategies for read-heavy applications where occasional retries cost less than systematic blocking.
- The Snailclimb/JavaGuide repository provides working implementations in
docs/java/concurrent/optimistic-lock-and-pessimistic-lock.mddemonstrating both approaches.
Frequently Asked Questions
What is the main difference between optimistic and pessimistic locking?
Pessimistic locking assumes conflicts will occur and blocks threads with exclusive locks before operations begin, while optimistic locking assumes conflicts are rare and proceeds without locks, verifying data integrity at commit time and retrying if necessary.
When should I use pessimistic locking over optimistic locking in Java?
Use pessimistic locking when your application has high write contention, requires atomic operations across multiple shared resources, or when the cost of retrying failed optimistic updates exceeds the cost of thread blocking and context switching.
Can optimistic locking cause deadlocks?
No, optimistic locking cannot cause deadlocks because it does not acquire or hold locks during the read-modify-write cycle. However, under extreme write contention, excessive retry loops can cause CPU spikes and livelock-like behavior where threads repeatedly fail CAS operations.
Which Java classes implement optimistic locking?
The java.util.concurrent.atomic package provides optimistic implementations including AtomicInteger, AtomicLong, and LongAdder. For database-style versioning, you manually implement version-field checks using SQL WHERE clauses or application-level version counters.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →