CAS and ABA problem generation and elegant solution

Keywords: Java Redis MongoDB MySQL

I collated Java advanced materials for free, including Java, Redis, MongoDB, MySQL, Zookeeper, Spring Cloud, Dubbo high concurrency distributed and other tutorials, a total of 30G, which needs to be collected by myself.
Transfer gate: https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q

 

Exclusive lock: it is a pessimistic lock. synchronized is an exclusive lock. It will cause all other threads that need to be locked to hang and wait for the thread holding the lock to release the lock.

Optimistic lock: every time no lock is added, assume there is no conflict to complete an operation. If the conflict fails, try again until it succeeds.

I. CAS operation

The mechanism of optimistic lock is CAS, Compare and Swap.

CAS has three operands, memory value V, old expected value A, and new value B to be modified. If and only if the expected value A is the same as the memory value V, change the memory value V to B, otherwise do nothing.

1. nonblocking algorithms

The failure or suspension of one thread should not affect the failure or suspension algorithm of other threads.

Modern CPU s provide special instructions that can automatically update shared data and detect interference from other threads. compareAndSet() uses these instead of locking.

2. Example of AtomicInteger

Take out AtomicInteger to study how to achieve data correctness without lock.

private volatile int value;

 

In the absence of lock mechanism, we need to use volatile primitive to ensure that the data between threads is visible (shared).

In this way, the value of the variable can be read directly.

public final int get() {
    return value;
}

 

Then let's see how + + i does it.

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

 

CAS operation is adopted here. Each time data is read from memory and CAS operation is performed on this data and the result after + 1. If successful, the result will be returned. Otherwise, retry until successful.

And compareAndSet uses JNI to complete the operation of CPU instructions.

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

 

The whole process is like this, using CAS instruction of CPU and JNI to complete Java non blocking algorithm. Other atomic operations are performed using similar characteristics.

The whole J.U.C is based on CAS, so for the synchronized blocking algorithm, J.U.C has greatly improved the performance. The article in the reference material introduces how to use CAS to build data structures such as non blocking counters and queues.

II. ABA problem

CAS looks great, but it can cause "ABA problems.".

An important premise of CAS algorithm implementation is to take out the data at a certain time in memory, and compare and replace at the next time, then the time difference class will lead to data changes.

For example, one thread takes a out of memory location V, while another thread two takes a out of memory, and two performs some operations to B, and then two changes the data of location V to A. at this time, thread one performs CAS operation and finds that the memory is still a, and then one succeeds. Although CAS operation of thread one is successful, it does not mean that this process is OK.

If the head of the linked list recovers its original value after changing twice, it does not mean that the linked list has not changed. So the atomic operation AtomicStampedReference/AtomicMarkableReference mentioned earlier is very useful. This allows atomic manipulation of a pair of changing elements.

There is a classic ABA problem in lock free operation with CAS:

Thread 1 is ready to replace the value of the variable with CAS from a to B. before that, thread 2 replaced the value of the variable with a to C and C to A. then thread 1 found that the value of the variable was still a when it executed CAS, so CAS succeeded. But in fact, the scene at this time is different from that at the beginning. Although CAS is successful, there may be potential problems, such as the following example:

There is a stack implemented by one-way linked list, with the top of the stack as a. at this time, thread T1 already knows that A.next is B, and then wants to replace the top of the stack with B by CAS:

head.compareAndSet(A,B);

Before T1 executes the above instruction, thread T2 intervenes to stack a and B, then push D, C and A. at this time, the stack structure is as follows, and object B is in free state:

At this time, it's thread T1's turn to perform CAS operation. It is detected that the top of the stack is still A, so CAS succeeds. The top of the stack changes to B, but in fact B.next is null, so the situation at this time changes to:

Among them, there is only one element B in the stack, and the linked list composed of C and d no longer exists in the stack, so C and D are lost without any reason.

The above is the hidden danger caused by the ABA problem. In the implementation of various optimistic locks, version stamps are usually used to mark records or objects to avoid the problems caused by concurrent operations. In Java, AtomicStampedReference < E > also implements this function. It marks version stamps for objects by wrapping tuples of [E,Integer], so as to avoid the ABA problem. For example, the following code AtomicInteger and atomicstampededreference are used to update the atomic integer variable with the initial value of 100 respectively. AtomicInteger will successfully perform CAS operation, while atomicstampededreference with version stamp will fail to perform CAS for ABA problem:

package concur.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABA {
    
    private static AtomicInteger atomicInt = new AtomicInteger(100);
    private static AtomicStampedReference<Integer> atomicStampedRef = 
            new AtomicStampedReference<Integer>(100, 0);
    
    public static void main(String[] args) throws InterruptedException {
        Thread intT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInt.compareAndSet(100, 101);
                atomicInt.compareAndSet(101, 100);
            }
        });
        
        Thread intT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean c3 = atomicInt.compareAndSet(100, 101);
                System.out.println(c3);        //true
            }
        });
        
        intT1.start();
        intT2.start();
        intT1.join();
        intT2.join();
        
        Thread refT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedRef.compareAndSet(100, 101, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
                atomicStampedRef.compareAndSet(101, 100, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
            }
        });
        
        Thread refT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedRef.getStamp();
                System.out.println("before sleep : stamp = " + stamp);    // stamp = 0
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
                System.out.println(c3);        //false
            }
        });
        
        refT1.start();
        refT2.start();
    }

}

 

Posted by chaotica on Tue, 17 Dec 2019 00:41:52 -0800