Java multi-threaded lock technology talk: optimistic lock VS pessimistic lock

Java multithreading technology has always been one of the core skills necessary for Java programmers. In Java multithreaded programming, in order to ensure data consistency and security, it is often necessary to use a lock mechanism to prevent multiple threads from modifying the same shared resource at the same time. Lock is a mechanism to achieve concurrent access control. When multiple threads jointly access shared resources, pessimistic locking is the most common way. However, in high concurrency scenarios, the concurrency blocking problem caused by global locks is also inevitable. of. In order to solve this problem, people have introduced the concept of optimistic locking. In this article, we will discuss optimistic and pessimistic locking in Java multithreading in detail.

1. What is a pessimistic lock

Pessimistic locking is a mechanism for multi-thread concurrency control. Pessimistic locks are generally implemented based on the implementation of database locks. When a thread wants to access a shared resource, it will use the mechanism defined by pessimistic locking to acquire the lock of the resource. During this process, if other threads also want to acquire the lock, they need to wait for the current thread to release the lock before acquiring the lock. Of course, you can also avoid problems such as deadlocks by setting timeouts and other mechanisms.

In Java, we can use the synchronized keyword to implement pessimistic locking. Synchronized can be used at the object level or at the class level. When a thread accesses a synchronized locked object, the state of the object will be set to the locked state. If other threads also want to modify this state, they must wait for the current thread to release the lock before acquiring the lock. This method seems ideal, but it is actually only for business scenarios with small concurrency.

public class LockExample {
    
    
    private int count = 0;

    public synchronized void increment() {
    
    
        // some code here
        this.count++;
    }
}

In the above code, the synchronized keyword is used to implement pessimistic locking. In the increment() method, the this keyword is used to lock the entire method to ensure that no other threads will access this method at the same time during the execution of a thread to this method.

The disadvantage of pessimistic locking is that because each thread needs to acquire a lock before accessing resources, when the concurrency is very high, it will cause a large number of threads to wait for the lock, which will cause the performance of the entire application to drop sharply.
In high-concurrency business scenarios, it is inappropriate to use pessimistic locks, which can easily cause problems such as deadlocks, resulting in degraded application performance.

2. What is an optimistic lock

If pessimistic locking is a defensive lock, then optimistic locking is an offensive lock that tries to avoid locking resources to the greatest extent. Optimistic locking embodies more of an "optimistic" idea. It believes that the probability of multiple threads accessing the same resource is not so high, so when accessing a shared resource, it does not perform a special locking operation on the resource. Instead, read and modify operations are performed directly on the resource, and version verification is performed after the modification operation is completed. If the read version number is consistent with the current version number, it means the operation is successful and the program continues to run, otherwise it means that other threads have modified the resource, you need to perform a Retry operation, read the version number again, and then update operate.

In Java, there are two main ways to implement optimistic locking, one is CAS, namely Compare And Swap, and the other is version number mechanism (such as AtomicInteger class).

Use the Compare-And-Swap algorithm

CAS is a mechanism for implementing atomic operations by allowing the CPU's underlying memory operation instructions. The operation principle of the CAS mechanism is to compare the value in the current memory with the value in the CAS instruction. If they are consistent, the value in the current memory will be updated to a new value. If they are not consistent, the operation will be re-executed.
The Compare-And-Swap (CAS) algorithm is an algorithm based on optimistic locking, which can achieve very high concurrency. The basic idea is that when we modify a shared resource, we first read the current state of the resource, and then compare the state. If the state has not changed, update the state of the resource; otherwise, ignore the modification operation, and Wait for the next opportunity to modify.

The following is an example of using the CAS algorithm to implement optimistic locking:

public class CASExample {
    
    
    private volatile int value;

    public void increment() {
    
    
        while (true) {
    
    
            // 使用CAS算法进行操作
            int current = this.value;
            int next = current + 1;
            if (compareAndSwap(current, next)) {
    
    
                break;
            }
        }
    }

    // 比较并替换方法
    private synchronized boolean compareAndSwap(int current, int next) {
    
    
        if (this.value == current) {
    
    
            this.value = next;
            return true;
        }
        return false;
    }
}

In this example, the increment() method continuously loops to obtain the value of the shared resource, and then determines whether the resource needs to be updated. When a resource needs to be updated, it calls the compareAndSwap() method to perform a compare and replace operation. If the current value is the same as the value we read, the new value replaces the old value.

It should be noted that in the Compare-And-Swap algorithm, the CAS algorithm can only play a real role when the shared resource is hot enough and the contention is fierce. Otherwise, because the CAS algorithm requires additional operations to obtain the status of the current resource , so its performance may be lower than the traditional pessimistic locking mechanism.

AtomicInteger class using version number mechanism

The version number mechanism, also known as the timestamp mechanism, is more common when performing cache operations such as Redis. The core idea is to add a version number field to each resource object that needs to be controlled, and the version number needs to be updated every time a modification operation is performed. If the modification is successful, the version number is +1, otherwise, no operation is performed. In this way, when performing the operation of reading the resource, you only need to compare the version number. If the version number is consistent, it means that subsequent operations can be performed, otherwise, it means that other threads have modified the resource and a Retry operation is required. .
The AtomicInteger class is thread-safe and can easily implement optimistic locking. In the following code, we use AtomicInteger to implement a counter, and use the incrementAndGet() method to implement the self-increment operation.

public class AtomicIntegerExample {
    
    
    private AtomicInteger count = new AtomicInteger();

    public void increment() {
    
    
        // some code here
        this.count.incrementAndGet();
    }
}

3. Comparison between pessimistic locking and optimistic locking

In actual development, pessimistic locking and optimistic locking have their own advantages and disadvantages, and developers need to choose flexibly according to specific business scenarios. Below, we will make a detailed comparison between pessimistic locking and optimistic locking.

3.1 Implementation Difficulty

The implementation of pessimistic locking is relatively simple, just need to introduce mechanisms such as the synchronized keyword into the code. The implementation of optimistic locking is more difficult. When using the CAS mechanism, you need to use some lower-level technologies. The entire implementation process is relatively cumbersome, and you need to carefully handle its boundary conditions, especially for high-concurrency scenarios.

3.2 Performance

In high concurrency scenarios, the performance of optimistic locking is often significantly better than that of pessimistic locking. This is because pessimistic locks perform frequent locking and unlocking operations on shared resources, which can easily cause thread blocking, resulting in system performance degradation. Therefore, in high concurrency scenarios, using optimistic locking can better improve the concurrency performance of the program.

3.3 Implementation Complexity

The implementation of pessimistic locking is essentially a locking/unlocking process. Optimistic locking needs to involve version control and other aspects. Therefore, in terms of implementation complexity, pessimistic locking is significantly lower than optimistic locking.

3.4 Handling of data conflicts

Pessimistic locking can handle data conflicts very well, because it always locks shared resources and excludes interference from other threads, thus effectively avoiding data conflicts. Optimistic locking needs to control the version number. If the version numbers are inconsistent, the resource needs to be retried. If there are too many retry times, problems such as retry timeout may occur, thereby affecting the normal operation of the system.

Four. Summary

From the content of the above comparison, we can see that pessimistic locking and optimistic locking have their own advantages and disadvantages. In actual development, developers need to make flexible choices according to specific business requirements, access frequency and other factors. Pessimistic locking can guarantee data consistency when the concurrency performance is low, but because each thread needs to acquire a lock when accessing resources, its performance may not be very good. In contrast, optimistic locking can use technologies such as the CAS algorithm to ensure the consistency of resources, thereby achieving higher concurrency performance. In high-concurrency scenarios, optimistic locking can better improve the concurrency performance of the program, but attention needs to be paid to controlling the version number to avoid excessive retries. Regardless of the locking mechanism, care must be taken to ensure data consistency and security, and avoid unsafe situations such as dirty reads, phantom reads, and out-of-order writes.

Additional code analysis

The complete code is as follows:

import java.util.concurrent.atomic.AtomicInteger;

public class LockExample {
    
    
    private int count1 = 0;
    private AtomicInteger count2 = new AtomicInteger();
    private volatile int count3;

    public synchronized void increment1() {
    
    
        this.count1++;
    }

    public void increment2() {
    
    
        this.count2.incrementAndGet();
    }

    public void increment3() {
    
    
        while (true) {
    
    
            int current = this.count3;
            int next = current + 1;
            if (compareAndSwap(current, next)) {
    
    
                break;
            }
        }
    }

    private synchronized boolean compareAndSwap(int current, int next) {
    
    
        if (this.count3 == current) {
    
    
            this.count3 = next;
            return true;
        }
        return false;
    }
}

In this code, we define a LockExample class, which contains three attributes count1, count2, and count3, which are used to demonstrate pessimistic locks, AtomicInteger optimistic locks, and optimistic locks of the CAS algorithm. These properties all implement an increment() method, which is used to perform self-increment operations on properties. Here, we use three different locking mechanisms, pessimistic locking using the synchronized keyword, optimistic locking using the AtomicInteger class, and optimistic locking using the CAS algorithm. Through the above code, we can better understand the lock mechanism in Java multithreading.

Guess you like

Origin blog.csdn.net/weixin_40986713/article/details/130972883
Recommended