Interviewer: What is an optimistic lock and what is a pessimistic lock?

1. Basic concepts

Optimistic locking and pessimistic locking are two ideas that are used to solve data competition problems in concurrent scenarios.

Optimistic locking: Optimistic locking is very optimistic when operating data, thinking that others will not modify the data at the same time. Therefore, the optimistic lock will not be locked, but when performing the update, judge whether someone else has modified the data during this period: if someone else has modified the data, the operation will be abandoned, otherwise the operation will be executed.
Pessimistic lock: Pessimistic lock is more pessimistic when operating data, thinking that others will modify the data at the same time. Therefore, the data is directly locked when the data is manipulated, and the lock will not be released until the operation is completed; other people cannot modify the data during the lock period.

2. Implementation (including examples)

Before explaining the implementation method, we need to be clear: optimistic locking and pessimistic locking are two ideas, and their use is very extensive, not limited to a certain programming language or database.

The realization of pessimistic lock is to lock, which can be either to lock the code block (such as the synchronized keyword of Java) or to lock the data (such as the exclusive lock in MySQL).

There are two main ways to implement optimistic locking: CAS mechanism and version number mechanism, which are described in detail below.

1. CAS (Compare And Swap) The
CAS operation includes 3 operands:


The expected value of the memory location (V) that needs to be read and
written (A) the new value to be written (B)
CAS operation logic is as follows: if the value of the memory location V is equal to the expected value of A, then the location is updated to the new Value B, otherwise do nothing. Many CAS operations are spinning: if the operation is unsuccessful, it will keep retrying until the operation is successful.

This raises a new question. Since CAS includes two operations, Compare and Swap, how does it guarantee atomicity? The answer is: CAS is an atomic operation supported by the CPU, and its atomicity is guaranteed at the hardware level.

Let's take the self-increment operation (i++) in Java as an example to see how pessimistic lock and CAS ensure thread safety. We know that the increment operation in Java is not an atomic operation, it actually contains three independent operations: (1) read the value of i; (2) add 1; (3) write the new value back to i

Therefore, if the self-increment operation is performed concurrently, the calculation result may be inaccurate. In the following code example: value1 does not have any thread safety protection, value2 uses optimistic locking (CAS), and value3 uses pessimistic lock (synchronized). Run the program and use 1000 threads to auto-increment value1, value2, and value3 at the same time. It can be found that the values ​​of value2 and value3 are always equal to 1000, and the value of value1 is often less than 1000.

public class Test {
    
    
     
    //value1:线程不安全
    private static int value1 = 0;
    //value2:使用乐观锁
    private static AtomicInteger value2 = new AtomicInteger(0);
    //value3:使用悲观锁
    private static int value3 = 0;
    private static synchronized void increaseValue3(){
    
    
        value3++;
    }
     
    public static void main(String[] args) throws Exception {
    
    
        //开启1000个线程,并执行自增操作
        for(int i = 0; i < 1000; ++i){
    
    
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    try {
    
    
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    value1++;
                    value2.getAndIncrement();
                    increaseValue3();
                }
            }).start();
        }
        //打印结果
        Thread.sleep(1000);
        System.out.println("线程不安全:" + value1);
        System.out.println("乐观锁(AtomicInteger):" + value2);
        System.out.println("悲观锁(synchronized):" + value3);
    }
}

First, let's introduce AtomicInteger. AtomicInteger is an atomic class provided by the java.util.concurrent.atomic package. It uses the CAS operation provided by the CPU to ensure atomicity; in addition to AtomicInteger, there are many atomic classes such as AtomicBoolean, AtomicLong, and AtomicReference.

Let's take a look at the source code of AtomicInteger to understand how its self-increment operation getAndIncrement() is implemented (the source code is Java7 as an example, Java8 is different, but the idea is similar).

public class AtomicInteger extends Number implements java.io.Serializable {
    
    
    //存储整数值,volatile保证可视性
    private volatile int value;
    //Unsafe用于实现对底层资源的访问
    private static final Unsafe unsafe = Unsafe.getUnsafe();
 
    //valueOffset是value在内存中的偏移量
    private static final long valueOffset;
    //通过Unsafe获得valueOffset
    static {
    
    
        try {
    
    
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
    
     throw new Error(ex); }
    }
 
    public final boolean compareAndSet(int expect, int update) {
    
    
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
 
    public final int getAndIncrement() {
    
    
        for (;;) {
    
    
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }
}

The source code analysis instructions are as follows:

(1) The auto-increment operation implemented by getAndIncrement() is a spin CAS operation: compareAndSet is performed in the loop, and if the execution succeeds, it exits, otherwise it has been executed.

(2) Among them, compareAndSet is the core of CAS operation, which is realized by using Unsafe object.

(3) Who is Unsafe? Unsafe is a class used to help Java access the underlying resources of the operating system (for example, it can allocate memory and release memory). Through Unsafe, Java has the underlying operating capabilities and can improve operating efficiency; the powerful underlying resource operating capabilities also bring security risks (The name of the class Unsafe also reminds us of this), so users cannot use it under normal circumstances. AtomicInteger uses the CAS function provided by Unsafe here.

(4) valueOffset can be understood as the offset of value in memory, which corresponds to V in the three operands (V/A/B) of CAS; the offset is also achieved through Unsafe.

(5) Volatile modifier in the value field: Java concurrent programming must ensure thread safety, and must ensure atomicity, visibility, and order; CAS operations can ensure atomicity, while volatile can ensure visibility and a certain degree of Sequence; in AtomicInteger, volatile and CAS together ensure thread safety. The explanation of the principle of volatile is related to the Java Memory Model (JMM), which is not detailed here.

After talking about AtomicInteger, let's talk about synchronized. Synchronized guarantees thread safety by locking the code block: at the same time, only one thread can execute the code in the code block. Synchronization is a heavyweight operation, not only because locking requires additional resources, but also because the switching of thread state involves the conversion of operating system core state and user state; however, as the JVM performs a series of optimizations on the lock (such as Spin locks, lightweight locks, lock coarsening, etc.), synchronized performance has been getting better and better.
2. Version number mechanism
In addition to CAS, the version number mechanism can also be used to implement optimistic locking. The basic idea of ​​the version number mechanism is to add a field version to the data to indicate the version number of the data. Whenever the data is modified, the version number is increased by 1. When a thread queries the data, the version number of the data is found together; when the thread updates the data, it is judged whether the current version number is consistent with the version number previously read, and the operation is performed if they are consistent.

It should be noted that the version number is used here as a marker for judging data changes. In fact, other fields that can mark the data version, such as timestamp, can be selected according to the actual situation.

Let's take "update the number of players' gold coins" as an example (the database is MySQL, the same is true for other databases) to see how the pessimistic lock and version number mechanism deal with concurrency issues.

Consider a scenario: the game system needs to update the player's gold coin count, and the updated gold coin count depends on the current state (such as the number of gold coins, level, etc.), so the player's current state needs to be queried before the update.

The following implementation does not provide any thread safety protection. If other threads update the player's information between query and update, the player's gold coin count will be inaccurate.

@Transactional
public void updateCoins(Integer playerId){
    
    
    //根据player_id查询玩家信息
    Player player = query("select coins, level from player where player_id = {0}", playerId);
    //根据玩家当前信息及其他信息,计算新的金币数
    Long newCoins = ……;
    //更新金币数
    update("update player set coins = {0} where player_id = {1}", newCoins, playerId);
}

In order to avoid this problem, pessimistic locking solves this problem by locking, the code is shown below. When querying player information, use select ...... for update to query; the query statement will add an exclusive lock to the player's data, and the exclusive lock will not be released until the transaction is submitted or rolled back; during this period, if other threads Attempts to update the player information or execute select for update will be blocked.

@Transactional
public void updateCoins(Integer playerId){
    
    
    //根据player_id查询玩家信息(加排它锁)
    Player player = queryForUpdate("select coins, level from player where player_id = {0} for update", playerId);
    //根据玩家当前信息及其他信息,计算新的金币数
    Long newCoins = ……;
    //更新金币数
    update("update player set coins = {0} where player_id = {1}", newCoins, playerId);
}

The version number mechanism is another way of thinking. It adds a field for player information: version. When querying the player information for the first time, the version information is also queried; when performing the update operation, check whether the version has changed, and if the version changes, no update is performed.

@Transactional
public void updateCoins(Integer playerId){
    
    
    //根据player_id查询玩家信息,包含version信息
    Player player = query("select coins, level, version from player where player_id = {0}", playerId);
    //根据玩家当前信息及其他信息,计算新的金币数
    Long newCoins = ……;
    //更新金币数,条件中增加对version的校验
    update("update player set coins = {0}, version = version + 1 where player_id = {1} and version = {2}", newCoins, playerId, player.version);
}

Three, advantages and disadvantages and applicable scenarios

Optimistic locking and pessimistic locking are not distinguished by their advantages and disadvantages. They have their own suitable scenarios; the following will explain from two aspects.

1. Functional limitations
Compared with pessimistic lock, the applicable scenarios of optimistic lock are more restricted, whether it is CAS or version number mechanism.

For example, CAS can only guarantee the atomicity of a single variable operation. When multiple variables are involved, CAS is powerless, while synchronized can be handled by locking the entire code block. Another example is the version number mechanism. If the query is for table 1 and the update is for table 2, it is difficult to achieve optimistic locking through a simple version number.

2. The degree of competition
If both pessimistic lock and optimistic lock can be used, then the choice should consider the degree of competition:

When the competition is not fierce (the probability of concurrency conflicts is small), optimistic locks are more advantageous, because pessimistic locks will lock code blocks or data, and other threads cannot access at the same time, which affects concurrency, and both locking and releasing locks need to consume extra Resources.
When the competition is fierce (the probability of concurrent conflicts is high), pessimistic locking is more advantageous, because optimistic locking fails frequently when performing updates, and requires constant retry, which wastes CPU resources.
Fourth, the interviewer asks: Is the optimistic lock locked?
During the interview, the author once encountered the interviewer asking questions like this. The following is my understanding of this problem:

(1) Optimistic lock itself is not locked, it just judges whether the data has been updated by other threads when updating; AtomicInteger is an example.

(2) Sometimes optimistic locking may cooperate with the locking operation. For example, in the aforementioned updateCoins() example, MySQL will add an exclusive lock when performing an update. But this is just an example of the cooperation between optimistic locking and locking operations, and cannot change the fact that "optimistic locking itself does not lock".

5. The interviewer asks: What are the disadvantages of CAS?

At this point in the interview, the interviewer may have liked you. However, the interviewer is ready to launch a final attack on you: Do you know the shortcomings of this implementation of CAS?

Here are some of the less perfect aspects of CAS:

1. ABA problem
Assuming there are two threads-thread 1 and thread 2, the two threads perform the following operations in order:

(1) Thread 1 reads the data in memory as A;

(2) Thread 2 modifies the data to B;

(3) Thread 2 modifies the data to A;

(4) Thread 1 performs CAS operations on data

In step (4), since the data in the memory is still A, the CAS operation is successful, but in fact the data has been modified by thread 2. This is the ABA problem.

In the case of AtomicInteger, ABA seems to be harmless. However, in some scenarios, ABA can bring hidden dangers, such as the stack top problem: the stack top of a stack has been changed twice (or multiple times) and then restored to its original value, but the stack may have changed.

For the ABA problem, a more effective solution is to introduce the version number. Every time the value in the memory changes, the version number is +1; when performing CAS operations, not only the value in the memory is compared, but the version number is also compared. The CAS can execute successfully only when there is no change. The AtomicStampedReference class in Java uses the version number to solve the ABA problem.

2. The cost problem under high competition.
In a high competition environment with a high probability of concurrent conflicts, if the CAS fails all the time, it will always try again, and the CPU overhead is high. One way of thinking about this problem is to introduce an exit mechanism, such as failing to exit after the number of retries exceeds a certain threshold. Of course, it is more important to avoid using optimistic locking in a highly competitive environment.

3. Functional limitations
The function of CAS is relatively limited. For example, CAS can only guarantee the atomicity of a single variable (or a single memory value) operation, which means: (1) Atomicity may not guarantee thread safety, for example, Java needs to cooperate with volatile to ensure thread safety; (2) When multiple variables (memory values) are involved, CAS is also helpless.

In addition, the implementation of CAS requires the support of the hardware-level processor. In Java, ordinary users cannot use it directly. They can only use the atomic classes under the atomic package, and their flexibility is limited.

Six, summary

This article introduces the basic concepts, implementation methods (including examples), applicable scenarios of optimistic lock and pessimistic lock, as well as the interviewer you may encounter. I hope it will be helpful to your interview. Finally, I wish you all get your favorite offer!

References: https://www.cnblogs.com/kismetv/p/10787228.html

Guess you like

Origin blog.csdn.net/weixin_44395707/article/details/106471414