-Do you know what CAS is?

Introduction to CAS

CAS is actually a frequent visitor, because it is the underlying principle of atomic classes and also the principle of optimistic locking, so when you go to an interview, you often encounter the question "What types of locks do you know?" You may answer "pessimistic locking and optimistic locking", then the next question is likely to ask about the principle of optimistic locking, which is related to CAS. Of course, you may continue to ask you about CAS application scenarios or shortcomings . problem.

First, let's take a look at what CAS is. Its English full name is Compare-And-Swap , and Chinese is called " Compare-And-Swap ". It is an idea and an algorithm.

In the case of multithreading, the execution order of each code cannot be determined, so in order to ensure concurrency safety, we can use mutex locks. The feature of CAS is to avoid the use of mutex locks. When multiple threads use CAS to update the same variable at the same time, only one of them can operate successfully, and the other threads will fail to update. However, unlike the synchronous mutex, the thread that failed to update will not be blocked, but will be told that the operation failed due to competition this time, but it can be tried again.

CAS is widely used in the field of concurrent programming to realize data exchange operations that will not be interrupted, thereby achieving lock-free thread safety.

The idea of ​​CAS

In most processor instructions, CAS-related instructions are implemented. This instruction can complete the "compare and exchange" operation. It is precisely because this is one (rather than multiple) CPU instructions, so CAS-related instructions Instructions are atomic, and this combined operation will not be interrupted during execution, so that concurrency safety can be guaranteed. Since this atomicity is guaranteed by the CPU, there is no need for our programmers to worry about it.

CAS has three operands: memory value V, expected value A, and value B to be modified. The core idea of ​​CAS is to modify the memory value to B only when the expected value A is the same as the current memory value V.

Let's expand on this: CAS will assume in advance that the current memory value V should be equal to the value A, and the value A is often the memory value V previously read at that time. When executing CAS, if it finds that the current memory value V is exactly the value A, then CAS will change the memory value V to the value B, and the value B is often after getting the value A, and then passing through the value A Calculated. If it is found that the memory value V is not equal to the value A during the execution of CAS, it means that the memory value has been modified by other threads during the calculation of B just now, so this CAS should not be modified anymore, which can avoid multiple people Modification at the same time resulted in an error. This is the main idea and process of CAS.

JDK uses these CAS instructions to implement concurrent data structures, such as atomic classes such as AtomicInteger.

The lock-free algorithm implemented by CAS is like when we negotiated, we used a very optimistic way to negotiate, and we were very friendly with each other. If we didn't negotiate this time, we can try again. The idea of ​​CAS is completely different from the previous mutual exclusion lock. If it is a mutual exclusion lock and there is no negotiation mechanism, everyone will try to seize the resource. If it is obtained, the resource will be firmly secured before the operation is completed. Hold it in your own hands. Of course, both the use of CAS and the use of mutex locks can guarantee concurrency safety, and they are different means to achieve the same goal.

example

Below we use diagrams and examples to make the CAS process clearer, as shown in the following figure:
Insert picture description here

Suppose there are two threads, each using two CPUs, they both want to use CAS to change the value of the variable on the right. Let's look at thread 1 first. It uses CPU 1. Assuming it executes first, it expects the current value to be 100 and wants to change it to 150. During execution, it will check if the current value is 100, and find that it is really 100, so it can be changed successfully, and when the change is completed, the value on the right will change from 100 to 150.

Insert picture description here
As shown in the figure above, suppose it’s just now the CPU 2 used by thread 2 to execute, and it wants to change this value from 100 to 200, so it also wants the current value to be 100, but in fact the current value is 150. Therefore, it will find that the current value is not what it expects, so it will not really continue to change 100 to 200, which means that the entire operation is ineffective. If the modification is not successful this time, the CAS operation fails.

Of course, the next thread 2 can also have other operations, which need to be determined according to business requirements, such as retrying, reporting an error, or skipping execution altogether. For example, in a spike scenario, multiple threads execute spikes at the same time, as long as one executes successfully, it is enough for the remaining threads to find that their CAS has failed, in fact, it means that the brother threads have executed successfully, and there is no need to continue execution. , This is the skip operation. Therefore, the business logic is different, there will be different processing methods, but no matter what the subsequent processing, the previous CAS operation has failed.

The semantics of CAS

Let's take a look at the semantics of CAS. With the equivalent code below, it will be easier to understand than the previous illustrations and text, because the code is actually clear at a glance. Next, we disassemble the CAS to see what exactly is done inside it. The equivalent semantic code of CAS is as follows:

/**
 * 描述:     模拟CAS操作,等价代码
 */
 
public class SimulatedCAS {
    
    
    private int value;
    public synchronized int compareAndSwap(int expectedValue, int newValue) {
    
    
        int oldValue = value;
        if (oldValue == expectedValue) {
    
    
            value = newValue;
        }
        return oldValue;
    }
}

In this code, there is a compareAndSwap method. There are two input parameters in this method. The first input parameter is expectedValue, and the second input parameter is newValue , which is the new value we calculated. We hope to add this The new value is updated to the variable.

You must have noticed that the compareAndSwap method is modified by synchronized . We use the synchronized method to ensure atomicity for the equivalent code of CAS.

Next, I will explain what is done in the compareAndSwap method. You need to get the current value of the variable first, so the code will use int oldValue = value to get the current value of the variable. Then there is compare, which is "comparison", so at this time, if (oldValue == expectedValue) is used to compare the current value with the expected value. If they are equal, it means that the current value is exactly the value we expect. If the conditions are met, it means that swap can be performed at this time, that is, exchange, so the value of value is modified to newValue, and finally it returns to oldValue, completing the entire CAS process.

The core idea of ​​CAS is reflected in the above process. It can be seen that compare refers to the comparison in if, comparing whether oldValue is equal to expectedValue; similarly, swap actually changes value to newValue and returns oldValue. So this entire compareAndSwap method restores the semantics of CAS, and also symbolizes the work done by the CAS command behind the scenes.

Case demonstration: Two threads compete for CAS, and one of them loses

With the equivalent code in front, let’s introduce a specific case in depth: two lines to execute CAS, try to modify the data, the first thread can be modified successfully, and the second thread will be found because it is late. If the data has been modified, it will not be modified anymore. We can see the specific situation of CAS in the execution process by debugging.

Below we use code to demonstrate what happens when CAS is competing between two threads. At the same time, I also recorded a video. You can also skip the text version to watch the video demonstration.

Let's look at the following piece of code:

public class DebugCAS implements Runnable {
    
    
    private volatile int value;
    public synchronized int compareAndSwap(int expectedValue, int newValue) {
    
    
        int oldValue = value;
        if (oldValue == expectedValue) {
    
    
            value = newValue;
            System.out.println("线程"+Thread.currentThread().getName()+"执行成功");
        }
        return oldValue;
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        DebugCAS r = new DebugCAS();
        r.value = 100;
        Thread t1 = new Thread(r,"Thread 1");
        Thread t2 = new Thread(r,"Thread 2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(r.value);
    }
    @Override
    public void run() {
    
    
        compareAndSwap(100, 150);
    }
}

The compareAndSwap method here is the equivalent semantic code of CAS, and then we add a line of code on this basis. If the execution is successful, it will print out which thread executed successfully.

In our main() method, first instantiate the DebugCAS class and modify the value of value to 100, so that its initial value is 100, and then we create two threads Thread t1 and Thread t2, and add them Start up, and the main thread waits for the completion of the two threads to print out the final value.

What did these two newly created threads do? As you can see in the run() method, the compareAndSwap method is executed, and the expected value is 100, and the desired value is 150. Then when both threads execute the run() method, it can be foreseen that , Only one thread will execute successfully, and the other thread will not print the sentence "executed successfully", because when it executes, you will find that the value at that time has been modified, not 100.

First of all, we do not interrupt the point, directly execute to see the results of the operation:

线程Thread 1执行成功
150

As you can see, Thread 1 is executed successfully, and the final result is 150. Here, the probability of printing the sentence "Thread 1 successfully executed" is much higher than the probability of printing the sentence "Thread 2 executed successfully", because Thread 1 is started first.

Let's use the debug method to see how it is executed internally. We first break at the line "if (oldValue == expectedValue){", and then run it in the form of Debug.
Insert picture description here

It can be seen that the program has stayed at the breakpoint at this time. It is Thread 1 (the name and status of the current thread can be displayed in the Debugger), and the status of Thread 2 at this time is Monitor (corresponding to the Java thread Blocked state), which means that the lock has not been synchronized and is waiting for the lock outside.

Now that Thread 1 enters the compareAndSwap method, we can clearly see that the value of oldValue is 100, and the value of expectedValue is also 100, so they are equal.

Continue to let the code run in a single step, because the if judgment condition is satisfied, so you can enter the if statement, so next, you will change the value to newValue, and the value of newValue is 150.

Insert picture description here
After the modification is completed, the sentence "Thread Thread 1 executed successfully" will also be printed, as shown in the figure below.

Insert picture description here
Next, when we press the execute button on the left, it is Thread 2's turn, and the situation is different at this time.
Insert picture description here
It can be seen that the value obtained by oldValue is 150, because the value of value has been modified by Thread 1 , so 150 is not equal to the value of 100 expected by Thread 2, so the entire if statement will be skipped. It will not print the sentence "Thread 2 successfully executed", and the oldValue will be returned in the end. In fact, no changes have been made to this value.

At this point, the two threads are executed. In the console, only the successful execution of Thread 1 is printed, but the successful execution of Thread 2 is not printed. The reason is already known through Debug.
Insert picture description here
The above code uses Debug to see that when two threads compete for CAS, one of them succeeds and the other fails.

Guess you like

Origin blog.csdn.net/Rinvay_Cui/article/details/111059089