1
头图
Author: Jacob Jankov
Original: http://tutorials.jenkov.com/java-concurrency/compare-and-swap.html
Translation: Pan If you have a better translation version, welcome ❤️ Submit issue or contribute~
Updated: 2022-02-24

CAS (compare and swap) is a technique used in the design of concurrent algorithms. Basically, CAS is comparing the value of the variable to the expected value, and if the values ​​are equal, swapping the value of the variable to the new value. CAS may sound a little complicated, but it's actually fairly simple once you understand it, so let me elaborate on this topic further.

By the way, compare and swap is sometimes short for CAS , so if you see some articles or videos about concurrency that mention CAS , it's most likely referring to the compare and swap operation.

CAS Tutorial Videos

If you like videos, I have a video tutorial version of this CAS here: (green internet)
CAS video tutorial

CAS usage scenarios (Check Then Act)

A common pattern in concurrent algorithms is the check-before-do ( check then act ) pattern. Check-before-do ( check then act ) pattern occurs when code first checks the value of a variable and then acts upon that value. Here is a simple example:

public class ProblematicLock {

    private volatile boolean locked = false;

    public void lock() {

        while(this.locked) {
            // 忙等待 - 直到 this.locked == false
        }

        this.locked = true;
    }

    public void unlock() {
        this.locked = false;
    }

}

This code is not a correct implementation of 100% for multithreaded locks. That's why I named it ProblematicLock (problem lock). However, I created this buggy implementation to illustrate how to work around it with the CAS feature.

The lock() method first checks whether the member variable locked is equal to false . This is done inside while-loop . If locked variable is false , then the lock() method leaves the while loop and sets locked to true . In other words, the lock() method first checks the value of the variable locked , and then acts on that check. Check first, then execute.

If multiple threads access the same ProblematicLock instance at almost the same time, the above lock() method will have some problems, for example:

If thread A checks that locked has a value of false (expected value), it will exit the while-loop loop and execute subsequent logic. If thread B also checks the value of locked before thread A sets the value of true to locked , then thread B will also exit the while-loop loop to execute subsequent logic. This is a typical resource contention problem.

Check Then Act must be atomic

In order to work properly in a multithreaded application (to avoid resource races), check-before-execute ( Check Then Act ) must be atomic. Atomicity means that both checking and executing actions are performed as atomic (indivisible) blocks of code. Any thread that starts executing the block will complete the block's execution without interference from other threads. No other threads are allowed to execute the same atomic block at the same time.

An easy way to make the Java code block atomic is to mark it with the Java keyword of synchronized . See about synchronized . Here is ProblematicLock the lock() method was converted to an atomic code block using the synchronized keyword before 06230641f8c5d8:

public class MyLock {

    private volatile boolean locked = false;

    public synchronized void lock() {

        while(this.locked) {
            // 忙等待 - 直到 this.locked == false
        }

        this.locked = true;
    }

    public void unlock() {
        this.locked = false;
    }

}

Now the method lock() has declared synchronization, so the lock() method of the same instance is only allowed to be accessed and executed by one thread at the same time. Equivalent to lock() method is atomic.

Blocking threads is expensive

When two threads try to enter a synchronized block in Java at the same time, one of the threads will be blocked and the other thread will be allowed to enter the synchronized block. When the thread that entered the synchronized block exits the block again, the waiting thread is allowed to enter the block.

If the thread is allowed access to execution, it is not very expensive to enter a synchronized block of code. But if another thread is forced to wait to block because there is already a thread executing in a synchronized block, then the cost of blocking the thread is very high.

Also, when the synchronized block becomes free again you cannot determine exactly when the blocked thread will be unblocked. This usually depends on the operating system or execution platform to coordination blocking threads blocking unblocking . Of course, it won't take seconds or minutes until the blocking thread is unblocked and allowed to enter, but some time may be wasted in blocking the thread since it would otherwise have access to a shared data structure. This is explained here:

Hardware-provided atomic CAS operations

Modern CPU built-in support for atomic operations on CAS . In some cases, the CAS operation can be used instead of a synchronized block or other blocking data structure. CPU guarantees that only one thread can perform CAS operations at a time, even across CPU cores. There is an example later in the code.

When using the CAS functionality provided by the hardware or CPU instead of the synchronized , lock , mutex (mutex locks), etc. provided by the operating system or execution platform, the operating system or execution platform does not need to handle the blocking and unblocking of threads. This allows threads using CAS to wait less time to perform operations, and have less congestion and higher throughput. As shown below:

As you can see, a thread trying to enter a shared data structure is never completely blocked. It keeps trying to perform the CAS operation until it succeeds and is allowed to access the shared data structure. This way the delay before a thread can enter a shared data structure is minimized.

Of course, if the thread waits a long time in the process of repeatedly executing CAS , it may waste a lot of CPU cycles that CPU have been used for other tasks (other threads). But in many cases, this is not the case. It depends on how long the shared data structure is used by another thread. In practice, shared data structures are not used for long, so the above shouldn't happen very often. But again it depends on the situation, the code, the data structure, the number of threads trying to access the data structure, the system load, etc. In contrast, blocked threads don't use CPU at all.

CAS in Java

Starting Java 5 , you can access the CAS methods at the CPU level through some new atomic classes in the java.util.concurrent.atomic package. These classes are:

The advantage of using the Java 5+ function that comes with CAS instead of implementing it yourself is that the Java 5+ function built into CAS allows your application to take advantage of the underlying capabilities of CPU to perform CAS operations. This makes your CAS implementation code faster.

Safeguarding of CAS

CAS feature can be used to protect a critical section ( Critical Section ), preventing multiple threads from executing the critical section at the same time.

?> critical section is the code that accesses critical resources in each thread. Whether it is a hardware critical resource or a software critical resource, multiple threads must access it mutually exclusive. The piece of code in each thread that accesses a critical resource is called a critical section ( Critical Section ). The part of the program in each thread that accesses a critical resource is called a critical section ( Critical Section ) (a critical resource is a shared resource that is only allowed to be used by one thread at a time). Only one thread is allowed to enter the critical section at a time, and other threads are not allowed to enter after entering.

An example below shows how the CAS function of the AtomicBoolean class can be used to implement the lock() method shown earlier and thus be guaranteed (only one thread at a time can exit the lock() method).

public class CompareAndSwapLock {

    private AtomicBoolean locked = new AtomicBoolean(false);

    public void unlock() {
        this.locked.set(false);
    }

    public void lock() {
        while(!this.locked.compareAndSet(false, true)) {
            // busy wait - until compareAndSet() succeeds
        }
    }
}

Note that this locked variable is no longer a boolean type but a AtomicBoolean type. This class has a compareAndSet() method that compares the instance's value (variable locked) with the first parameter ( false ). If the comparison result is the same (that is, locked The value is equal to the first parameter false), then the instance value locked will be exchanged with the expected value true (that is, the locked variable is set to true, indicating that it is locked). If the exchange is successful, the compareAndSet() method will return true , and if the exchange is not successful, it will return false .

In the above example, the compareAndSet() method call compares the locked variable value with the false value, and if the resulting value of the locked variable value is false , then the locked value is set to true .

Since only one thread is allowed to execute the compareAndSet() method at a time, only one thread will be able to see the AtomicBoolean instance value of false and thus swap it for true . Therefore, only one thread can exit while-loop (while loop) at a time, by calling unlock() method to set locked to false so that only one thread's CompareAndSwapLock is unlocked at a time.

CAS implements optimistic locking

It is also possible to use the CAS feature as an optimistic locking mechanism. Optimistic locking allows multiple threads to enter a critical section at the same time, but only allows one of them to submit its work when the critical section ends.

Here is an example of a concurrent counter class that uses an optimistic locking strategy:

public class OptimisticLockCounter{

    private AtomicLong count = new AtomicLong();


    public void inc() {

        boolean incSuccessful = false;
        while(!incSuccessful) {
            long value = this.count.get();
            long newValue = value + 1;

            incSuccessful = this.count.compareAndSet(value, newValue);
        }
    }

    public long getCount() {
        return this.count.get();
    }
}

Note how the inc() method gets the existing count value from the AtomicLong instance variable count . The new value is then calculated based on the old value. Finally, the inc() method attempts to set the new value by calling the compareAndSet() method of the AtomicLong instance.

If AtomicLong instance value count still has the same value at the time of the last fetch ( long value = this.count.get() ) when compared, then compareAndSet() will execute successfully. But if another thread has called and increased the instance value of AtomicLong at the same time (meaning that a thread has successfully called the compareAndSet() method before, which is generally considered to be resource competition ), then the compareAndSet() call will fail because the expected value value is not Again equal to the value stored in AtomicLong (the original value has been changed by the previous thread). In this case, the inc() method will do another iteration in while-loop (while loop) and try to increment AtomicLong value again.

(End of this article)

Original: http://tutorials.jenkov.com/java-concurrency/compare-and-swap.html
Translation: Pan If you have a better translation version, welcome ❤️ Submit issue or contribute~

潘潘和他的朋友们
94 声望110 粉丝