Multithreading 7: Detailed Explanation of Optimistic Lock and Pessimistic Lock

If pessimistic locks and optimistic locks correspond to real life. Pessimistic locking is a bit like a pessimistic (or a rainy day) person who always assumes the worst and avoids problems. Optimistic locking is a bit like an optimistic person who always assumes the best situation and solves problems quickly before problems arise.

In the programming world, the ultimate goal of optimistic locking and pessimistic locking is to ensure thread safety and avoid resource competition in concurrent scenarios. However, pessimistic locking has a greater impact on performance than optimistic locking!

What is a pessimistic lock? What is the usage scenario?

Pessimistic locking always assumes the worst case, thinking that there will be problems every time a shared resource is accessed (such as shared data being modified), so it will be locked every time it acquires a resource operation, so that other threads want to get it The resource will block until the lock is released by the previous holder.

That is to say, shared resources are only used by one thread at a time, other threads are blocked, and the resources are transferred to other threads after use .

Exclusive locks like Java synchronizedand ReentrantLockothers are the realization of pessimistic locking ideas.

Pessimistic locks are usually used when there are many writes (multiple write scenarios) to avoid frequent failures and retries that affect performance.

What is optimistic locking? What is the usage scenario?

Optimistic locking always assumes the best situation, thinking that there will be no problems every time a shared resource is accessed, and the thread can execute continuously without locking or waiting. It only verifies the corresponding resource when submitting the modification ( That is, whether the data) has been modified by other threads (the specific method can use the version number mechanism or the CAS algorithm).

The atomic variable class under the package in Java is implemented java.util.concurrent.atomicusing CAS , an implementation method of optimistic locking.

Optimistic locks are usually more than the case of relatively few writes (multi-read scenario), avoiding frequent locking to affect performance, and greatly improving the throughput of the system.

How to implement optimistic locking?

Optimistic locking is generally implemented using the version number mechanism or the CAS algorithm. There are relatively more CAS algorithms, so special attention is required here.

version number mechanism

Generally, a data version number field is added to the data table versionto indicate the number of times the data has been modified. When the data is modified, versionthe value is incremented by one. When thread A wants to update the data value, it will also read the value while reading the data version. When submitting the update, if the value of the version just read versionis equal to the value in the current database, it will be updated, otherwise retry the update operation , until the update is successful.

Give a simple example : Suppose there is a version field in the account information table in the database, the current value is 1; and the current account balance field ( balance) is $100.

  1. Operator A reads it at this point ( version=1 ) and deducts $50 from his account balance ( $100-$50 ).
  2. During operator A's operation, operator B also reads in this user information ( version=1 ) and deducts $20 from his account balance ( $100-$20 ).
  3. Operator A has completed the modification work, and submits the data version number ( version=1 ), together with the balance after account deduction ( balance=$50 ), to the database update. At this time, since the submitted data version is equal to the current version of the database record, the data is updated, and the database record versionUpdated to 2.
  4. versionOperator B completed the operation, and tried to submit the data ( =$80 ) to the database with the version number ( balance=1 ), but at this time, when comparing the database record version, it was found that the data version number submitted by operator B was 1, and the database record is currently The version is also 2, which does not satisfy the optimistic locking strategy of "the commit version must be equal to the current version to perform the update", so the commit of operator B is rejected.

This avoids the possibility that operator B versionoverwrites the operation result of operator A with the result modified by the old data based on =1.

CAS algorithm

The full name of CAS is Compare And Swap (Comparison and Exchange) , which is used to implement optimistic locking and is widely used in major frameworks. The idea of ​​CAS is very simple. It is to compare an expected value with the variable value to be updated, and update when the two values ​​are equal.

CAS is an atomic operation, and the bottom layer relies on a CPU atomic instruction.

Atomic operation is the smallest indivisible operation, that is to say, once the operation starts, it cannot be interrupted until the operation is completed.

CAS involves three operands:

  • V : The variable value to update (Var)
  • E : Expected value (Expected)
  • N : the new value to be written (New)

CAS atomically updates the value of V with the new value N if and only if the value of V is equal to E. If not, it means that other threads have updated V, and the current thread gives up the update.

Take a simple example : thread A wants to modify the value of variable i to 6, and the original value of i is 1 (V = 1, E = 1, N = 6, assuming that there is no ABA problem).

  1. i is compared with 1, if they are equal, it means that it has not been modified by other threads, and can be set to 6.
  2. Compare i with 1, if they are not equal, it means that it has been modified by other threads, the current thread gives up the update, and the CAS operation fails.

When multiple threads use CAS to operate a variable at the same time, only one will win and update successfully, and the rest will fail, but the failed thread will not be suspended, but will be notified of failure and allowed to try again, of course. A failed thread is allowed to abort the operation.

The Java language does not directly implement CAS, and the CAS-related implementation is implemented in the form of C++ inline assembly (JNI call). Therefore, the specific implementation of CAS is related to the operating system and CPU.

sun.miscThe classes under the package Unsafeprovide compareAndSwapObject, compareAndSwapInt, compareAndSwapLongmethods to implement CAS operations on Object, int, longtypes

/**
	*  CAS
  * @param o         包含要修改field的对象
  * @param offset    对象中某field的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

For Unsafea detailed introduction to the class, you can read this article: Detailed Explanation of the Java Magic Class Unsafe - JavaGuide - 2022 .

What are the problems with optimistic locking?

The ABA problem is the most common problem with optimistic locking.

ABA questions

If a variable V is the value of A when it is first read, and it is checked that it is still the value of A when it is ready to be assigned, can we tell that its value has not been modified by other threads? Obviously not, because its value may be changed to other values ​​during this period, and then changed back to A, then the CAS operation will mistakenly think that it has never been modified. This problem is known as the "ABA" problem for CAS operations .

The solution to the ABA problem is to add a version number or timestamp in front of the variable . The classes after JDK 1.5 AtomicStampedReference are used to solve the ABA problem. The compareAndSet()method is to first check whether the current reference is equal to the expected reference, and whether the current flag is equal to the expected flag. If all are equal, the value of the reference and the flag is atomically Set to the given updated value.

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    
    
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

Long cycle times and high overhead

CAS often uses spin operations to retry, that is, if it fails, it will loop until it succeeds. If it is unsuccessful for a long time, it will bring a very large execution overhead to the CPU.

If the JVM can support the pause instruction provided by the processor, the efficiency will be improved to a certain extent. The pause instruction has two functions:

  1. The execution of instructions in the pipeline can be delayed so that the CPU does not consume too much execution resources. The delay time depends on the specific implementation version. On some processors, the delay time is zero.
  2. It can prevent the CPU pipeline from being emptied due to sequential memory flushing when exiting the loop, thereby improving the execution efficiency of the CPU.

Atomic operations can only be guaranteed for one shared variable

CAS is only valid for a single shared variable, and CAS is invalid when the operation involves multiple shared variables. But starting from JDK 1.5, classes are provided AtomicReferenceto ensure the atomicity between referenced objects. You can put multiple variables in one object for CAS operations. So we can use locks or use classes AtomicReferenceto combine multiple shared variables into A shared variable to operate on.

Guess you like

Origin blog.csdn.net/qq_35385687/article/details/129148225