Optimistic vs Pessimistic Locking
Question Variations
- "Explain the difference between optimistic and pessimistic locking."
- "When would you prefer optimistic locking over pessimistic locking in a web application?"
- "How do you implement optimistic concurrency control in a database?"
- "What are the risks of using pessimistic locking in a high-traffic environment?"
Why This Is Asked
This tests your understanding of data consistency in concurrent systems. Interviewers want to see that you can reason about trade-offs between throughput and correctness, and that you can pick the right strategy based on contention levels.
Key Concepts
- Pessimistic locking acquires a lock before reading, blocking other writers (and sometimes readers)
- Optimistic locking allows concurrent reads but checks for conflicts at write time (e.g., version columns, ETags)
- Pessimistic: high correctness guarantees, lower throughput, risk of deadlocks
- Optimistic: high throughput under low contention, but requires conflict resolution logic
- Real-world implementations:
SELECT FOR UPDATE(pessimistic), row version /If-Matchheaders (optimistic)
Question Variations
- “Explain the difference between optimistic and pessimistic locking.”
- “When would you prefer optimistic locking over pessimistic locking in a web application?”
- “How do you implement optimistic concurrency control in a database?”
- “What are the risks of using pessimistic locking in a high-traffic environment?”
Answers by Technology
+ Add VariantExpected Answer (Java 26)
Java provides several mechanisms for concurrency control, ranging from low-level primitives to high-level abstractions in java.util.concurrent.
- Pessimistic Locking:
synchronizedkeyword: Easiest to use, monitor-based locking.ReentrantLock: More flexible, supports timeouts (tryLock) and fairness.
- Optimistic Locking:
Atomicclasses (e.g.,AtomicInteger): Use Compare-and-Swap (CAS) instructions.StampedLock: Provides an “optimistic read” mode that doesn’t block writers.
- Database Level: Using JPA
@Versionannotation for optimistic concurrency control.
Why It Matters
Java is heavily used for high-concurrency systems. Knowing when to use synchronized vs Atomic vs ReentrantLock is critical for building performant, thread-safe applications. Over-locking leads to contention, while under-locking leads to data corruption.
Code Example
// Optimistic Locking with StampedLock
private final StampedLock lock = new StampedLock();
private int balance;
public int getBalanceOptimistic() {
long stamp = lock.tryOptimisticRead();
int currentBalance = balance;
if (!lock.validate(stamp)) { // Check if a write happened
stamp = lock.readLock(); // Fallback to pessimistic read
try {
currentBalance = balance;
} finally {
lock.unlockRead(stamp);
}
}
return currentBalance;
}
Common Mistakes
- Lock Contention: Using a single global lock for a high-traffic resource.
- Deadlocks: Acquiring multiple locks in different orders across different threads.
- Forgetting to unlock: Always use
finallyblocks when using explicitLockobjects.
Follow-up Questions
- CAS (Compare-and-Swap): How does it work? (Answer: A hardware-level instruction that updates a value only if it matches an expected value, returning success or failure).
- Intrinsic vs Extrinsic Locks? (Answer: Intrinsic locks are built into every Java object (
synchronized); Extrinsic locks are manual classes likeReentrantLock).
Expected Answer (PHP 8.5)
Because PHP follows a Shared Nothing architecture (each request has its own memory space), concurrency control is usually handled externally:
- Database Locking: Using
FOR UPDATEin SQL to acquire a pessimistic lock within a transaction. - Distributed Locks (Redis): Using a library like
redlock-phpto acquire a lock across multiple web servers. - Advisory Locks: Database-level locks that aren’t tied to rows (e.g.,
GET_LOCK()in MySQL orpg_advisory_lock()in PostgreSQL). - File Locking:
flock()can be used for single-server locking but is rarely used in modern cloud environments.
Why It Matters
Standard PHP code cannot use thread-based locks (like Mutex or synchronized). If two users try to claim the same username simultaneously, the race condition must be resolved at the storage layer or via a distributed locking service.
Code Example
// Pessimistic Locking with Eloquent (Laravel)
DB::transaction(function () {
$account = Account::where('id', 1)->lockForUpdate()->first();
if ($account->balance >= 100) {
$account->balance -= 100;
$account->save();
}
});
Common Mistakes
- Race conditions in PHP code: Attempting to check a condition and then update in two separate, non-atomic steps (e.g.,
if ($x) { $db->update(); }). - Deadlocks: Acquiring multiple row locks in different orders in separate requests.
Follow-up Questions
- How does Laravel’s Atomic Lock work? (Answer: It uses a cache driver (like Redis or Memcached) to store a key with a TTL (Time-To-Live) to simulate a lock).
- Why is ‘Shared Nothing’ beneficial for scaling? (Answer: It makes web servers horizontal and stateless, as no state is shared in the application’s memory).
Expected Answer
Concurrency control is managed through two primary strategies:
- Pessimistic Locking: Locks the record when it is read. Other processes must wait for the lock to be released before they can read or write. It is best for high-contention scenarios (e.g., financial transactions).
- Optimistic Locking: Allows concurrent reads and only checks for conflicts at write time. It usually uses a version number or timestamp. If the version has changed since it was read, the write fails and the process must retry. It is best for low-contention, read-heavy workloads.
Why It Matters
Choosing the wrong locking strategy can lead to severe performance issues. Over-using pessimistic locking in a high-traffic web app causes bottlenecks and deadlocks, where requests hang waiting for locks. Conversely, failing to handle concurrency in a financial system leads to the Lost Update problem, where two users overwrite each other’s changes, resulting in incorrect balances or data corruption.
Example Code
Optimistic Locking (SQL)
-- Transaction 1 reads: balance=100, version=5
SELECT balance, version FROM Accounts WHERE id = 1;
-- Transaction 1 attempts update:
UPDATE Accounts
SET balance = 150, version = 6
WHERE id = 1 AND version = 5;
-- If 0 rows affected, a conflict occurred. The app should retry.
Pessimistic Locking (SQL)
BEGIN TRANSACTION;
-- Locks the row until the transaction commits or rolls back
SELECT balance FROM Accounts WHERE id = 1 FOR UPDATE;
UPDATE Accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;
Common Mistakes
- Defaulting to Pessimistic Locking: In most distributed systems, contention is low. Pessimistic locks reduce throughput and increase the risk of deadlocks across network partitions.
- Forgetting Retry Logic: Optimistic locking only works if the application layer is designed to catch conflict errors and implement a retry strategy (e.g., exponential backoff).
- Holding locks during I/O or user input: Keeping a database lock open while waiting for an external API call or a user to click “Submit” will block other users for seconds or even minutes.
Follow-up Questions
- How do you implement distributed locking across multiple nodes? (Answer: Use a coordination service like Redis (Redlock), ZooKeeper, or etcd to manage lock ownership across the cluster).
- What is MVCC? (Answer: Multi-Version Concurrency Control allows multiple versions of a row to exist simultaneously, letting readers see a snapshot of the data without being blocked by writers).