Talk about concurrency -- CAS algorithm

1, Atomic class

1. CAS algorithm

It is strongly recommended that readers read this article before reading it   First acquaintance with JUC   The first two sections have a preliminary understanding of atomicity, atomic variables and memory visibility.

CAS (Compare and Swap) is a hardware support for concurrency. It is a special instruction in the processor designed for multiprocessor operation. It is used to manage concurrent access to shared data. It is the hardware support for concurrent operation of shared data. It is an atomic operation, and the corresponding CPU instruction is cmpxchg. It is a CPU concurrency primitive.
CAS contains three operands: memory value V, comparison value A and update value B. If and only if V == A, V = B, otherwise no operation is performed.
CAS algorithm: when multiple threads modify the data in main memory concurrently. There is only one thread that will succeed, and the others will fail (at the same time, the operation will only fail and will not be locked).
CAS is a lock free non blocking algorithm and an implementation of optimistic lock. There is no context switching problem.
CAS is more efficient than ordinary synchronization lock. Reason: when CAS algorithm fails this time, it will not block next time, that is, it will not give up the execution right of CPU. It can try again immediately and update again.
Generally speaking, i want to change the variable i from 2 to 3. When i == 2 in the memory and the modification is successful, it is successful. If i in memory is no longer 2 due to the operation of other threads, my modification this time is regarded as a failure.

2. Simple use

After JDK 1.5, common atomic variables are provided in the java.util.concurrent.atomic package. It supports lockless thread safe programming on a single variable. These atomic variables have the following characteristics: volatile memory visibility; CAS algorithm ensures the atomicity of data.

atomic package description: image source API document

Code example: atomic variable usage

 1 public class Main {
 2     public static void main(String[] args) {
 3         AtomicInteger integer = new AtomicInteger(2);
 4 
 5         boolean b = integer.compareAndSet(3, 5);
 6         System.out.println(b);
 7         System.out.println(integer.get());
 8 
 9         b = integer.compareAndSet(2, 10);
10         System.out.println(b);
11         System.out.println(integer.get());
12 
13         // Equivalent to i++
14         integer.getAndIncrement();
15 
16         // Equivalent to ++i
17         integer.incrementAndGet();
18     }
19 }
20 
21 // result
22 false
23 2
24 true
25 10

Analysis: very simple, set the initial value to 2.
① change from 3 to 5, and set the initial memory value to 2, so the modification fails and returns false.
② change from 2 to 10. The initial memory value is 2, so the modification is successful and returns true.

3. Source code analysis

The underlying of these atomic variables is to ensure the atomicity of data through CAS algorithm.
Source code example: AtomicInteger class

 1 public class AtomicInteger extends Number implements java.io.Serializable {
 2     private static final long serialVersionUID = 6214790243416807050L;
 3 
 4     // setup to use Unsafe.compareAndSwapInt for updates
 5     private static final Unsafe unsafe = Unsafe.getUnsafe();
 6     private static final long valueOffset;
 7 
 8     // obtain value Address offset in memory
 9     static {
10         try {
11             valueOffset = unsafe.objectFieldOffset
12                 (AtomicInteger.class.getDeclaredField("value"));
13         } catch (Exception ex) { throw new Error(ex); }
14     }
15 
16     private volatile int value;
17 
18     public AtomicInteger(int initialValue) {
19         value = initialValue;
20     }
21 
22     public AtomicInteger() {
23     }
24 
25     public final int get() {
26         return value;
27     }
28 
29     public final void set(int newValue) {
30         value = newValue;
31     }
32 
33     public final boolean compareAndSet(int expect, int update) {
34         return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
35     }
36 
37     public final int getAndIncrement() {
38         return unsafe.getAndAddInt(this, valueOffset, 1);
39     }
40 
41     public final int incrementAndGet() {
42         return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
43     }
44 
45 }

Description: public final boolean compareAndSet(int expect, int update)
Variable valueOffset: obtain the offset address of variable value in memory through static code block.
Variable value: decorated with volatile, which reflects "memory visibility between multiple threads".
this: the AtomicInteger object itself.
It's easy to understand: change the variable value of this of the current object from expect ed value to update.

Source code example: Unsafe class

 1 public final class Unsafe {
 2 
 3     public native void throwException(Throwable var1);
 4 
 5     public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
 6 
 7     public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
 8 
 9     public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
10 
11     public native int getIntVolatile(Object var1, long var2);
12 
13 
14     public final int getAndAddInt(Object var1, long var2, int var4) {
15         int var5;
16         do {
17             // Get object var1 Variable of var2 Memory value for
18             var5 = this.getIntVolatile(var1, var2);
19         } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
20 
21         return var5;
22     }
23 
24 }

Unsafe is the core class of CAS, and all its methods are modified by native. In other words, the methods in the unsafe class directly call the underlying resources of the operating system to perform corresponding tasks. They are local methods written in C/C + +. The implementation of CAS algorithm is also realized by unsafe class directly operating specific memory data by calling local methods.
The getAndIncrement() method can ensure the atomicity and self increment of variables in a multi-threaded environment. However, the synchronized or lock lock lock is not added in the source code, so how is it guaranteed? It's actually very simple:

First obtain the memory value of a variable, and then compare and update it through CAS algorithm. If it fails, it keeps trying again. It is a cyclic process, which is also called spin.
This is why the autoincrement operation of AtomicInteger is atomic.

1 private AtomicInteger i = new AtomicInteger();
2 public int getI() {
3     return i.getAndIncrement();
4 }

4. Disadvantages of CAS

(1) ABA problem.
(2) longer cycle time: when using CAS, some threads may fail to modify the cycle all the time, resulting in longer cycle time, which will bring great execution overhead to the CPU. Because the variables in AtomicInteger are volatile, in order to ensure memory visibility, it is necessary to ensure cache consistency and transmit data through the bus. When there are a large number of CAS cycles, a bus storm will occur.
(3) atomic operation of only one variable can be guaranteed: it is impossible to ensure the atomicity of multiple variable operations. In this case, you can only use the Lock tool in the synchronized or juc package.

2, ABA problem

1. Introduction

Code example: demonstrates ABA issues

 1 // Atomic reference class demo ABA problem
 2 public class ABATest {
 3     public static void main(String[] args) throws InterruptedException {
 4         AtomicReference<String> reference = new AtomicReference<>("A");
 5 
 6         // thread  t1 from A modify B,Again by B modify A
 7         new Thread(() -> {
 8             System.out.println(reference.compareAndSet("A", "B") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
 9             System.out.println(reference.compareAndSet("B", "A") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
10         }, "t1").start();
11 
12 
13         new Thread(() -> {
14             // Give Way t1 Thread completion ABA operation
15             try {
16                 Thread.sleep(500);
17             } catch (InterruptedException e) {
18                 e.printStackTrace();
19             }
20             System.out.println(reference.compareAndSet("A", "C") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
21 
22         }, "t2").start();
23 
24         Thread.sleep(1000);
25 
26         System.out.println(reference.get());
27     }
28 }
29 
30 // result
31 true. t1 value is:B
32 true. t1 value is:A
33 true. t2 value is:C
34 C

How to understand ABA problem?
You may think that thread t2 is to change "A" to "C". Although the middle has changed, it has no effect on t2!
For example, your bank card has 10w. In the middle, you receive 1w of salary, and then you are deducted from the mortgage for 1w. At this time, your bank card is still 10w. Although the result has not changed, the balance is not the original balance. Moreover, you must care where your money goes in the middle, so it's different.
Another example: for the company's finance, at some point, the account is 100w. You secretly misappropriate 20w of public funds, and then make it up quietly. Although the result has not changed, we are concerned about the accounting details in the middle, because you have broken the law at this time.

2. Settle

Atomic reference with time stamp: Java provides atomic stamped reference to solve ABA problem. In fact, the version number is added. For each modification, the version number is + 1. The comparison is whether the memory value + version number is consistent.
Code example: solving ABA problems

 1 public class ABATest {
 2     public static void main(String[] args) throws InterruptedException {
 3 
 4         AtomicStampedReference<String> reference = new AtomicStampedReference<>("A", 1);
 5         final int stamp = reference.getStamp();
 6 
 7         // thread  t1 from A modify B,Again by B modify A
 8         new Thread(() -> {
 9             System.out.println(reference.compareAndSet("A", "B", stamp, stamp + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
10             System.out.println(reference.compareAndSet("B", "A", reference.getStamp(), reference.getStamp() + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
11         }, "t1").start();
12         
13 
14         new Thread(() -> {
15             // Give Way t1 Thread completion ABA operation
16             try {
17                 Thread.sleep(500);
18             } catch (InterruptedException e) {
19                 e.printStackTrace();
20             }
21             System.out.println(reference.compareAndSet("A", "C", stamp, stamp + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
22 
23         }, "t2").start();
24 
25         Thread.sleep(1000);
26 
27         System.out.println(reference.getReference());
28     }
29 }
30 
31 // result
32 true. t1 value is:B
33 true. t1 value is:A
34 false. t2 value is:A    // t2 No modification succeeded
35 A

Four parameters of compareAndSet() method:

expectedReference: indicates the expected reference value
newReference: indicates the new reference value to be modified
expectedStamp: indicates the expected stamp (version number)
newStamp: indicates the new stamp (version number) after modification

3. Source code analysis

 1 public class AtomicStampedReference<V> {
 2 
 3     private static class Pair<T> {
 4         final T reference;
 5         final int stamp;
 6         private Pair(T reference, int stamp) {
 7             this.reference = reference;
 8             this.stamp = stamp;
 9         }
10         static <T> Pair<T> of(T reference, int stamp) {
11             return new Pair<T>(reference, stamp);
12         }
13     }
14     
15     public boolean compareAndSet(V   expectedReference,
16                                  V   newReference,
17                                  int expectedStamp,
18                                  int newStamp) {
19         Pair<V> current = pair;
20         return
21             expectedReference == current.reference &&
22             expectedStamp == current.stamp &&
23             ((newReference == current.reference &&
24               newStamp == current.stamp) ||
25              casPair(current, Pair.of(newReference, newStamp)));
26     }
27 
28     private boolean casPair(Pair<V> cmp, Pair<V> val) {
29         return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
30     }
31 }

Very simple. A Pair of pairs are maintained. In addition to reference, there is also an int type stamp (version number). When comparing updates, both variables should be compared.

3, LongAdder

1. Introduction

LongAdder is recommended in Alibaba Java development manual.

AtomicLong essentially means that multiple threads operate the same target resource at the same time. If only one thread succeeds, other threads will fail. If you keep trying again (spin), spin will become a bottleneck.
The idea of LongAdder is to [scatter] the target resources to be operated into the array Cell. Each thread performs atomic operations on the value of its own Cell variable, greatly reducing the number of failures.
This is why LongAdder is recommended in high concurrency scenarios.

Reference documents: https://www.matools.com/api/java8
Alibaba Java development manual Baidu online disk: https://pan.baidu.com/s/1aWT3v7Efq6wU3GgHOqm-CA Password: uxm8

Posted on Wed, 01 Dec 2021 23:11:53 -0500 by zackat