[Dead Java Concurrency] - In-depth Analysis of CAS

Posted by dabnet on Thu, 26 Sep 2019 06:24:47 +0200

Table of Contents

CAS analysis

CAS defect

CAS, Compare And Swap, that is, compare and exchange. Doug lea uses CAS technology to implement concurrent operation of Java multithreading in synchronous components. The whole AQS synchronization components, Atomic atomic class operations, etc. are implemented in CAS, and even Concurrent HashMap is adjusted to CAS+Synchronized in version 1.8. It can be said that CAS is the cornerstone of the whole JUC.

CAS analysis

There are three parameters in CAS: memory value V, old expected value A, and value B to be updated. If and only if the value of memory value V equals the old expected value A, the value of memory value V will be changed to B, otherwise nothing will be done. Its pseudocode is as follows:

if(this.value == A){
    this.value = B
    return true;
}else{
    return false;
}

The atomic classes under JUC are all implemented by CAS. The following is an example of Atomic Integer to illustrate the implementation of CAS. As follows:

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

Unsafe is the core class of CAS. Java can not access the underlying operating system directly, but through local methods. Nevertheless, the JVM opens a back door: Unsafe, which provides hardware-level atomic operations.

valueOffset is the offset address of variable value in memory, unsafe is to get the original value of data by offset address.

The current value of value, modified with volatile, ensures that the same is seen in a multithreaded environment.

We will use Atomic Integer's addAndGet() method to illustrate, first look at the source code:

    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

The getAndAddInt method of unsafe is called internally. In getAndAddInt method, we mainly look at compareAndSwapInt method:

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

This method is a local method, which has four parameters: the object, the address of the object, the expected value and the modified value (a partner told me what these four variables mean when he interviewed...). The implementation of this method is not described in detail here. Interested partners can see the source code of openjdk.

CAS guarantees that a read-rewrite operation is an atomic operation, which is easy to implement on a single processor, but a little complicated to implement on a multi-processor.

CPU provides two ways to implement multiprocessor atomic operations: bus locking or cache locking.

  • Bus Locking: Bus Locking is the use of a LOCK # signal provided by the processor. When a processor outputs this signal on the bus, requests from other processors will be blocked, so the processor can use shared memory exclusively. But this kind of processing method seems a bit arbitrary, unkind, he locked the communication between CPU and memory, during the lock period, other processors can not other memory address data, its overhead is a little bit big. So there's a cache lock.
  • Cache Locking: In fact, for the above situation, we just need to ensure that the operation of a memory address at the same time is atomic. Cache locking means that if the data cached in the memory area is written back to memory during the lock operation, the processor does not output LOCK signal, but modifies the internal memory address, using the cache consistency protocol to ensure atomicity. Cache consistency mechanism can ensure that the data in the same memory area can only be modified by one processor. That is to say, when CPU1 modifies the i in the cached rows, CPU2 can not cache the i cached rows at the same time.

CAS defect

CAS solves atom operation efficiently, but there are still some shortcomings, mainly in three ways: too long cycle time, only one shared variable atom operation, ABA problem.

Too long cycle time

What if CAS has been unsuccessful? This situation is absolutely possible, if spin CAS is unsuccessful for a long time, it will bring great CPU overhead. Some places in JUC limit the number of CAS spins, such as the Synchronous Queue of BlockingQueue.

Only one shared variable atomic operation can be guaranteed

Seeing the implementation of CAS, we know that it can only be used for one shared variable. If there are multiple shared variables, we can only use locks. Of course, if you have a way to integrate multiple variables into one variable, it's good to use CAS. For example, the high status of state in read-write locks

ABA problem

CAS needs to check whether the operation value has changed, and update if it has not. But there is such a situation: if a value turns from A to B and then to A, it will not change when CAS checks, but in essence it has changed, which is the so-called ABA problem. The solution to ABA problem is to add a version number, that is, to add a version number to each variable, and add 1 to each change, that is, A - > B - > A, to become 1A - > 2B - > 3A.

The solution to the ABA problem of CAS is version number. Java provides Atomic Stamped Reference to solve it. Atomic StampedReference avoids ABA problems by wrapping tuples of [E,Integer] to stamp versions of objects. Thread 1 should fail in the above case.

The compareAndSet() method of AtomicStampedReference is defined as follows:

    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

CompeAndSet has four parameters: expected reference, updated reference, expected flag and updated flag. The Source Department understands well that the expected reference= the current reference, the expected identity= the current identity, and returns true directly if the updated reference and flag are equal to the current reference and flag, otherwise a new pair object is generated through Pair to replace the current pair CAS. Pair is an internal class of Atomic StampedReference. It is mainly used to record reference and version stamp information (identification). It is defined as follows:

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;

Pair records object references and version stamps. The version stamps are int type and keep increasing. At the same time, Pair is an immutable object, all its attributes are defined as final, providing an out of method, which returns a new Pari s object. Pair object is defined as volatile to ensure visibility in multi-threaded environment. In Atomic Stamped Reference, most methods generate a new Pair object by calling Pair of method and assign it to the variable pair. For example, set method:

    public void set(V newReference, int newStamp) {
        Pair<V> current = pair;
        if (newReference != current.reference || newStamp != current.stamp)
            this.pair = Pair.of(newReference, newStamp);
    }

Next we will show an example of the difference between Atomic Stamped Reference and Atomic Integer. We define two threads, thread 1 is responsible for 100 - > 110 - > 100, thread 2 is responsible for executing 100 - > 120, see the difference between the two.

public class Test {
    private static AtomicInteger atomicInteger = new AtomicInteger(100);
    private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);

    public static void main(String[] args) throws InterruptedException {

        //AtomicInteger
        Thread at1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInteger.compareAndSet(100,110);
                atomicInteger.compareAndSet(110,100);
            }
        });

        Thread at2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(2);      // at1, Execution Completed
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("AtomicInteger:" + atomicInteger.compareAndSet(100,120));
            }
        });

        at1.start();
        at2.start();

        at1.join();
        at2.join();

        //AtomicStampedReference

        Thread tsf1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //Let tsf2 get stamp first, resulting in inconsistent anticipated timestamps
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // Expected Reference: 100, Updated Reference: 110, Expected Identity getStamp() Updated Identity getStamp() +1
                atomicStampedReference.compareAndSet(100,110,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
                atomicStampedReference.compareAndSet(110,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
            }
        });

        Thread tsf2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedReference.getStamp();

                try {
                    TimeUnit.SECONDS.sleep(2);      //Thread tsf1 Executed
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("AtomicStampedReference:" +atomicStampedReference.compareAndSet(100,120,stamp,stamp + 1));
            }
        });

        tsf1.start();
        tsf2.start();
    }

}

Operation results:

The running results fully demonstrate the ABA problem of Atomic Integer and the ABA problem solved by Atomic Stamped Reference.

Topics: Java jvm