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:
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicReference
- AtomicStampedReference
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
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~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。