20 What Is the Nature of Pessimistic Locks and Optimistic Locks

20 What Is the Nature of Pessimistic Locks and Optimistic Locks #

In this lesson, we will talk about pessimistic locking and optimistic locking.

First, let’s understand how pessimistic locking and optimistic locking are classified. Pessimistic locking and optimistic locking are classified based on whether or not resources are locked.

Pessimistic Locking #

Pessimistic locking is quite pessimistic. It assumes that if the resource is not locked, other threads will compete for it, resulting in incorrect data. Therefore, to ensure the correctness of the result, pessimistic locking locks the data every time it is accessed and modified, preventing other threads from accessing the data. This ensures the integrity of the data.

This is similar to the personality of a pessimist in humans. Pessimists are always worried and cautious before doing things, so they guard and protect their things to prevent others from touching them. This is the meaning behind the name “pessimistic locking”.

img

Let’s take an example. Let’s assume that both thread A and thread B are using pessimistic locking, so they must acquire the lock when trying to access the synchronized resource.

img

Let’s assume that thread A acquires the lock and is currently operating on the synchronized resource. At this point, thread B must wait.

img

After thread A finishes execution, the CPU wakes up thread B, which was waiting for the lock, to try acquiring the lock again.

img

If thread B now acquires the lock, it can perform its own operations on the synchronized resource. This is the operation flow of pessimistic locking.

Optimistic Locking #

Optimistic locking is quite optimistic. It believes that no other threads will interfere when operating on resources, so it does not lock the object being operated on, allowing other threads to access it. However, to ensure data correctness, it compares the data before updating to check if it has been modified by other threads during the update process. If the data has not been modified, it means that only the current thread is operating on it, and it can proceed with the update. If the data has been modified and is different from what the thread initially obtained, it means that another thread has modified the data in the meantime, so the current thread will abandon the update and choose a strategy such as error reporting or retrying.

This is similar to the personality of an optimist in human beings. Optimists do not worry about things that have not happened yet; on the contrary, they believe that the future is bright. Therefore, they do not lock the data before modifying it. However, optimists do not act blindly. If they find that things are different from their expectations, they will have corresponding ways to handle it. They will not just sit back and wait. This is the philosophy of optimistic locking.

img

Optimistic locking is usually implemented using the CAS (Compare and Swap) algorithm. Let’s take an example. Let’s assume that thread A is currently using optimistic locking. When it operates on the synchronized resource, it does not need to acquire the lock in advance. It directly reads the synchronized resource and performs calculations within its own thread.

img

Before updating the synchronized resource, it checks whether the resource has been modified by other threads.

img

If at this point the synchronized resource has not been modified by other threads, meaning that the data is still consistent with what thread A initially obtained, then thread A proceeds with the update, completing the modification process.

img

However, if the synchronized resource has been modified by other threads at this point and is different from what thread A initially obtained, then thread A realizes that it is too late to make the modification and decides to handle it according to different business logic, such as reporting an error or retrying.

The concepts of pessimistic locking and optimistic locking are not unique to Java. This is a broad concept that can be applied to other fields as well, such as databases, which also apply pessimistic locking and optimistic locking.

Typical Cases #

  • Pessimistic Lock: synchronized keyword and Lock interface

The implementation of pessimistic lock in Java includes the synchronized keyword and related classes in Lock interface. Let’s take Lock interface as an example, for instance, the lock() method in the implementation class ReentrantLock is used to acquire the lock, and the unlock() method is used to release the lock. Before accessing the resource, the lock must be acquired, and after finishing the processing, the lock is released. This is a typical pessimistic lock mindset.

  • Optimistic Lock: Atomic classes

An atomic class, such as AtomicInteger, is a typical example of optimistic lock. When updating data, the optimistic lock mindset is used, and multiple threads can operate on the same atomic variable simultaneously.

  • Extreme happiness and extreme sorrow: Database

Both pessimistic and optimistic lock mindsets can coexist in databases. For example, if we use the select for update statement in MySQL, it is using pessimistic lock, which means the data cannot be modified by third parties before it is committed. This can result in performance overhead and is not recommended in high-concurrency scenarios.

On the other hand, we can implement optimistic lock in a database by utilizing a version field. No lock is required when retrieving or modifying data. However, after retrieving the data, performing calculations, and preparing to update, we check if the version number is still the same as when the data was retrieved. If they are the same, we proceed with the update. If not, it means that the data has been modified by another thread during the computation, so we can choose to re-retrieve the data, recalculate, and attempt to update again.

An example SQL statement is as follows (assuming the version is 1 when retrieving the data):

UPDATE student
SET 
    name = '小李',
    version = 2
WHERE
    id = 100
    AND version = 1

“Your honey, their poison” #

There is a belief that pessimistic lock is less performant than optimistic lock because it involves heavy operations, prevents parallel execution of multiple threads, and incurs context switching. However, this belief is inaccurate.

Although pessimistic lock does indeed block threads that cannot acquire the lock, this fixed cost is consistent. The initial overhead of pessimistic lock is indeed higher than that of optimistic lock, but it is a one-time cost. Even if a thread continuously fails to acquire the lock, there will be no additional impact on the overhead.

In contrast, although the initial overhead of optimistic lock is smaller than that of pessimistic lock, if a thread continuously fails to acquire the lock or if there is high concurrency and intense competition, resulting in constant retries, the consumed resources will increase and the overhead may even exceed that of pessimistic lock.

Therefore, the effectiveness of pessimistic lock can vary greatly depending on different scenarios. It may be a good choice in one scenario but a poor choice in another, just like “your honey, their poison”.

Therefore, let’s take a look at the suitable scenarios for each type of lock and allocate the appropriate lock to the corresponding scenario in order to allocate resources reasonably.

Suitable Scenarios for Each Type of Lock #

Pessimistic lock is suitable for scenarios with high concurrency of write operations, complex critical section code, and intense competition. In such scenarios, pessimistic lock can avoid a large number of useless repeated attempts and other overhead.

Optimistic lock is suitable for scenarios where the majority of operations are read operations and only a few are modification operations. It is also suitable for scenarios with high read and write concurrency but not intense competition. In these scenarios, the lock-free feature of optimistic lock can significantly improve performance.