ABA problem and solution under concurrent programming CAS

Posted by buraks78 on Tue, 19 Oct 2021 02:26:47 +0200

Catalogue of series articles

1: Computer model & detailed explanation of volatile keyword
2: Lock system in java
3: synchronized keyword explanation
5: Atomic atomic class and Unsafe magic class
6: ABA problems and solutions under CAS

1, CAS problem introduction

In the concurrency problem, the first thought is undoubtedly mutually exclusive synchronization, but thread blocking and wake-up bring great performance problems. The core of synchronization lock is to prevent the problems caused by concurrent modification of shared variables, but there is not such a competitive relationship at any time.

2, What is CAS

CAS, compare and swap (CAS). If the expected value is the same as the main memory value, exchange the value to be updated, also known as optimistic lock. It is a common optimistic lock mechanism to reduce read-write lock conflict and ensure data consistency.

If thread A copies the variable A from the main memory to 1, and changes the copy A to 10 in its own thread, when thread A is ready to update this variable to the main memory, if the value of main memory A does not change (expected value) or 1, then thread A successfully updates the value of A in the main memory. However, if the value of main memory A has been changed by other threads and is not 1, thread A will try again and again until it succeeds (spin)
The idea of CAS is very simple: three parameters: the current memory value V, the old expected value A and the value B to be updated. If and only if the expected value A is the same as the memory value V, modify the memory value to B and return true. Otherwise, do nothing and return false

3, ABA problem analysis

1. Introduction of problems

  • Bank deposit and withdrawal

  • Thieves steal money


2. ABA problem code

 static AtomicInteger atomicInteger = new AtomicInteger(1);
    public static void main(String[] args) {
       CompletableFuture.runAsync(()->{
           int a = atomicInteger.get();
           System.out.println("Operation thread"+Thread.currentThread().getName()+"--Operation value before modification:"+a);
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           boolean isCasSuccess = atomicInteger.compareAndSet(a,2);
           if(isCasSuccess){
               System.out.println("Operation thread"+Thread.currentThread().getName()+"--Cas Operation value after modification:"+atomicInteger.get());
           }else{
               System.out.println("CAS Modification failed");
           }
       });

        CompletableFuture.runAsync(()->{
            atomicInteger.incrementAndGet();// 1+1 = 2;
            System.out.println("Operation thread"+Thread.currentThread().getName()+"--increase Post value:"+atomicInteger.get());
            atomicInteger.decrementAndGet();// atomic-1 = 2-1;
            System.out.println("Operation thread"+Thread.currentThread().getName()+"--decrease Post value:"+atomicInteger.get());
        });

    }

Execution result: the value obtained by thread 2 is equal to 1. At this time, the value obtained by thread 3 is also 1. At this time, + 1 is equal to 2, and then minus 1 is equal to 1. At this time, thread 1 updates. The expected value is 1 and the updated value is 2, so the update is successful. However, thread 3 has operated on this value twice during this period.

3. The problem caused by ABA in concurrent business scenario has been solved

The reason for the ABA problem is that the CAS process simply verifies the "value". In some cases, the same "value" will not introduce wrong business logic (such as order inventory). In some cases, although the "value" is the same, it is no longer the original data

3.1 problems

In the system, the total inventory of a commodity is 5, and user 1 and user 2 place orders at the same time
User 1 purchased 3 inventories, so the inventory should be set to 2
User 2 purchased 2 inventories, so the inventory should be set to 3
The two interfaces for setting inventory are executed concurrently. The inventory will first become 2 and then 3, resulting in inconsistent data (5 items are actually sold, but only 2 is deducted from the inventory. The last inventory setting will overwrite and mask the previous concurrent operation)

3.2 reasons

The root cause of data inconsistency is that the inventory and the queried inventory are not checked when the setting operation occurs. Theoretically:
Only when the inventory is 5 can the inventory setting 2 of user 1 succeed
Only when the inventory is 5 can the inventory setting 3 of user 2 succeed

During actual implementation:
The inventory is 5, and the set stock 2 of user 1 should indeed succeed

Inventory has changed to 2. set stock 3 of user 2 should fail

3.3 solution

In the above example, when performing the update operation, you can bring the version number

4, ABA problem solving

Optimization direction: CAS can not only compare "values", but also ensure that the original data can be modified successfully.

1,AtomicStampReference

AtomicStampReference adds a marker stamp on the basis of cas. Using this marker can be used to detect whether the data has changed and bring a kind of effectiveness test to the data. It has the following parameters:

//The meanings represented by parameters are expected value, new value written, expected tag and new tag value
public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp);
public V getRerference();
public int getStamp();
public void set(V newReference,int newStamp);

2. Usage examples of atomicstamppreference

    private static AtomicStampedReference<Integer> atomicStampedRef =
            new AtomicStampedReference<>(1, 0);
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        CompletableFuture.runAsync(()->{
            int stamp = atomicStampedRef.getStamp(); //Get current tag identification
            System.out.println("Operation thread" + Thread.currentThread()+ "stamp="+stamp + ",Initial value a = " + atomicStampedRef.getReference());
            try {
                Thread.sleep(1000); //Wait 1 second for the interfering thread to execute
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1);  //At this time, the expectedReference is not changed, but the stamp has been modified, so CAS fails
            System.out.println("Operation thread" + Thread.currentThread() + "stamp="+stamp + ",CAS Operation results: " + isCASSuccess);
            countDownLatch.countDown();
        });
        CompletableFuture.runAsync(()->{
            int stamp = atomicStampedRef.getStamp();
            atomicStampedRef.compareAndSet(1,2,stamp,stamp+1);
            System.out.println("Operation thread" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",[increment] ,value = "+ atomicStampedRef.getReference());
            stamp = atomicStampedRef.getStamp();
            atomicStampedRef.compareAndSet(2,1,stamp,stamp+1);
            System.out.println("Operation thread" + Thread.currentThread() + "stamp="+atomicStampedRef.getStamp() +",[decrement] ,value = "+ atomicStampedRef.getReference());
            countDownLatch.countDown();
        });
        countDownLatch.await();
    }

Execution result: the value obtained by thread 2 is equal to 1, and the tag value is also 0. At this time, the value obtained by thread 3 is also 1, and the tag value is also 0. At this time, + 1 is equal to 2, and the tag value + 1 is 1; Then minus 1 equals 1, and the tag value + 1 equals 2. At this time, thread 1 updates. The expected value is 1, the tag value is 0, and the update value is 2, so the update cannot succeed

summary

In fact, in addition to the AtomicStampedReference class, there is another atomic class that can also be solved, that is, AtomicMarkableReference. Instead of maintaining a version number, it maintains a boolean type tag. Its usage is not as flexible as AtomicStampedReference. Therefore, it is only used in specific scenarios

Topics: Java Concurrent Programming