Interview must ask series: talk about optimistic lock and pessimistic lock!

foreword

Optimistic locking and pessimistic locking are frequently asked interview questions. This article will gradually introduce their basic concepts, implementation methods (including examples), applicable scenarios, and questions from interviewers you may encounter, hoping to help you impress the interviewers.

content

1. Basic Concepts

2. Implementation (including examples)

3. Advantages and disadvantages and applicable scenarios

Fourth, the interviewer asked: Is the optimistic lock locked?

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

6. Summary

1. Basic Concepts

Optimistic locking and pessimistic locking are two ideas 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 only to determine whether others have modified the data during the update: if others have modified the data, the operation will be abandoned, otherwise the operation will be performed.

Pessimistic lock : Pessimistic lock is pessimistic when operating data, thinking that others will modify the data at the same time. Therefore, the data is directly locked when operating the data, and the lock is not released until the operation is completed; other people cannot modify the data during the locking period.

2. Implementation (including examples)

Before explaining the implementation, it needs 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 .

Pessimistic locking is implemented by locking, which can be either locking code blocks (such as Java's synchronized keyword) or locking data (such as exclusive locks in MySQL).

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

1、CAS(Compare And Swap)

The CAS operation consists of 3 operands :

  • Memory location to read and write (V)
  • Expected value for comparison (A)
  • New value to be written (B)

The CAS operation logic is as follows: if the value of memory location V is equal to the expected value of A, update the location to the new value B, otherwise do nothing. Many CAS operations are spin: if the operation is unsuccessful, it will be retried until the operation succeeds.

This leads to a new question. Since CAS includes two operations, Compare and Swap, how does it ensure 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 auto-increment operation (i++) in Java as an example to see how pessimistic locking and CAS ensure thread safety respectively. We know that the auto-increment operation in Java is not an atomic operation, it actually consists of three independent operations:

  • read the value of i;
  • plus 1;
  • write the new value back to i

Therefore, if the auto-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 locking (synchronized).

Run the program and use 1000 threads to perform auto-increment operations on value1, value2 and value3 at the same time. It can be found that the values ​​of value2 and value3 are always equal to 1000, while 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 to introduce AtomicInteger. AtomicInteger is an atomic class provided by the java.util.concurrent.atomic package, which 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 auto-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 is explained as follows :

  • The self-increment operation implemented by getAndIncrement() is a spin CAS operation: compareAndSet is performed in a loop, and if the execution is successful, it exits, otherwise it is always executed.

  • The compareAndSet is the core of the CAS operation, which is implemented using the Unsafe object.

  • Who is Unsafe? Unsafe is a class used to help Java access the underlying resources of the operating system (such as memory allocation and release). Through Unsafe, Java has the ability to operate at the bottom level, which can improve operating efficiency; the powerful ability to operate underlying resources also brings security risks (The class name Unsafe also reminds us of this), so it is not available to the user under normal circumstances. AtomicInteger uses the CAS functionality provided by Unsafe here.

  • 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 obtained through Unsafe.

  • The volatile modifier of the value field: To ensure thread safety in Java concurrent programming, atomicity, visibility and ordering need to be guaranteed; CAS operations can guarantee atomicity, while volatile can guarantee visibility and a certain degree of ordering; In AtomicInteger, volatile and CAS together guarantee thread safety. The description of the action principle of volatile involves the Java Memory Model (JMM), which is not detailed here.

After talking about AtomicInteger, let's talk about synchronized. Synchronized ensures thread safety by locking the code block: at a time, only one thread can execute the code in the code block. Synchronized is a heavyweight operation, not only because locking requires additional resources, but also because the switching of thread states involves the conversion of operating system kernel state and user state ; Spin locks, lightweight locks, lock coarsening, etc.), the performance of synchronized has gotten 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, which represents the version number of the data. Whenever the data is modified, the version number is incremented by 1. When a thread queries 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 read before, and the operation is performed only if they are consistent.

It should be noted that the version number is used here as the mark 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 player gold coins" as an example (the database is MySQL, and the same is true for other databases) to see how the pessimistic lock and version number mechanisms deal with concurrency problems.

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

The following implementation does not perform any thread safety protection. If another thread updates the player's information between query and update, it will cause the player's gold count to 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);
}

To avoid this problem, pessimistic locking solves this problem by locking, the code is as follows. When querying player information, use select ... for update to query; the query statement will add an exclusive lock to the player data, and the exclusive lock will not be released until the transaction is committed 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, which adds a field to player information: version. When the player information is queried for the first time, the version information is also queried; when the update operation is performed, it is checked whether the version has changed. If the version has changed, the update will not be 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} where player_id = {1} and version = {2}", newCoins, playerId, player.version);
}

3. Advantages and disadvantages and applicable scenarios

There is no difference between optimistic locking and pessimistic locking, they have their own suitable scenarios; the following two aspects will be explained.

1. Functional limitations

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

For example, CAS can only guarantee the atomicity of operations on a single variable, 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. Intensity of competition

If both pessimistic and optimistic locking can be used, then the choice depends on the degree of competition:

  • When the competition is not fierce (the probability of concurrency conflict is small) , optimistic locking is more advantageous, because pessimistic locking will lock code blocks or data, other threads cannot access at the same time, affecting concurrency, and locking and releasing locks require additional consumption resource of.

  • When the competition is fierce (the probability of concurrency conflicts is high) , pessimistic locks are more advantageous, because optimistic locks frequently fail when performing updates, requiring constant retry and wasting CPU resources.

Fourth, the interviewer asked: Is the optimistic lock locked?

When I was interviewing, I encountered this question from the interviewer. Here is my understanding of the problem:

  1. Optimistic locking itself is not locked , but only to determine whether the data has been updated by other threads when updating; AtomicInteger is an example.

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

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

At this point in the interview, the interviewer may already like you. But the interviewer is ready to launch a final attack on you: do you know any downsides to this implementation of CAS?

Here are some of the less-than-perfect places in CAS:

1. ABA problem

Assuming there are two threads - thread 1 and thread 2, the two threads do the following in order:

  • Thread 1 reads the data in memory as A;
  • Thread 2 modifies the data to B;
  • Thread 2 modifies the data to A;
  • Thread 1 performs a CAS operation on the data

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

In the case of AtomicInteger, ABA doesn't seem to be doing any harm. However, in some scenarios, ABA will bring hidden dangers, such as the stack top problem: the stack top of a stack has been changed twice (or more) and the original value has been restored, but the stack may have changed.

For the ABA problem, a more effective solution is to introduce a version number. Every time the value in the memory changes, the version number is +1; when performing a CAS operation, not only the value in the memory is compared, but also the version number is compared. CAS can only be executed successfully when neither of them has changed. The AtomicStampedReference class in Java uses the version number to solve the ABA problem.

2. Overhead problem under high competition

In a high-competition environment with a high probability of concurrent conflict, if CAS fails all the time, it will keep retrying, resulting in high CPU overhead. One idea for this problem is to introduce an exit mechanism, such as failure to exit after the number of retries exceeds a certain threshold. Of course, it is more important to avoid optimistic locking in high contention environments.

3. Functional limitations

The function of CAS is relatively limited. For example, CAS can only guarantee the atomicity of operations on a single variable (or a single memory value), which means: (1) Atomicity does not necessarily guarantee thread safety. volatile cooperates to ensure thread safety; (2) when multiple variables (memory values) are involved, CAS is powerless.

In addition, the implementation of CAS requires the support of the processor at the hardware level. In Java, ordinary users cannot use it directly, but can only use the atomic class under the atomic package, which limits the flexibility.

6. Summary

This article introduces the basic concepts, implementation methods (including examples), applicable scenarios, and possible questions of interviewers of optimistic locking and pessimistic locking, hoping to help you in your interview. Finally, I wish you all the best of luck!

{{o.name}}
{{m.name}}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=324074007&siteId=291194637