[JDK source code] concurrent atomic class AtomicStampedReference

Keywords: Java JDK Back-end

brief introduction

AtomicStampedReference is an atomic class provided under the java concurrency package. It can solve ABA problems that cannot be solved by other atomic classes, such as AtomicInteger ABA problem

ABA

The ABA problem occurs in a multithreaded environment. When a thread reads the same memory address twice in a row and gets the same value twice, it simply thinks that "the value of this memory address has not been modified". However, at the same time, another thread may change the value of this memory address from a to B and back to A. at this time, it simply thinks that "Not modified" is obviously wrong.

For example, two threads execute in the following order:

(1) Thread 1 reads the value of memory location X as A;

(2) Thread 1 is blocked;

(3) Thread 2 reads the value of memory location X as A;

(4) Thread 2 modifies the value of memory location X to B;

(5) Thread 2 modifies the value of memory location X to A;

(6) Thread 1 recovers and continues to execute. After comparison, it is found that A sets the value of memory location X to C;

It can be seen that for thread 1, the first A and the second A are not actually the same A.

ABA problems usually occur in a lock free structure. The above process is represented by code, which is roughly like this:

public static void main(String[] args) {
    AtomicInteger atomicInteger = new AtomicInteger(1);
    new Thread(new Runnable() {
        @Override
        public void run() {
            int value = atomicInteger.get();
            System.out.println("Thread 1 read value="+value);
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (atomicInteger.compareAndSet(value,3)) {
                System.out.println("Thread 1 modification value from"+value+"To 3");
            }
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            int value = atomicInteger.get();
            System.out.println("Thread 2 read value="+value);
            if (atomicInteger.compareAndSet(value,2)) {
                System.out.println("Thread 2 modification value from"+value+"To 2");

                value = atomicInteger.get();
                System.out.println("Thread 2 read value="+value);
                if (atomicInteger.compareAndSet(value,1)){
                    System.out.println("Thread 2 modification value from"+value+"To 1");
                }
            }
        }
    }).start();
}

output

Thread 1 read value=1
 Thread 2 read value=1
 Thread 2 modification value From 1 to 2
 Thread 2 read value=2
 Thread 2 modification value From 2 to 1
 Thread 1 modification value From 1 to 3

Inner class

AtomicStampedReference

private static class Pair<T> {
    final T reference;// Element value
    final int stamp;  // Version number
    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);
    }
}

Bind the element value and version number together and store them in the reference and stamp (meaning of stamp and stamp) of Pair.

Construction method

public AtomicStampedReference(V initialRef, int initialStamp) {
    // That is, the element value and version number must be passed in during initialization
    pair = Pair.of(initialRef, initialStamp);
}

compareAndSet() method

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    // Gets the current (element value and version number) pair
    Pair<V> current = pair;
    return
        // The element hasn't changed
        expectedReference == current.reference &&
        // The version number has not changed
        expectedStamp == current.stamp &&
        // The new reference is equal to the old reference
        ((newReference == current.reference &&
          // The new version number is equal to the old version number
          newStamp == current.stamp) ||
         // Construct a new Pair object and update it
         casPair(current, Pair.of(newReference, newStamp)));
}
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
private static final long pairOffset =
    objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    // Call the compareAndSwapObject() method of Unsafe to update the reference of pair as a new reference
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

(1) If the element value and version number have not changed and are the same as the new one, return true;

(2) If the element value and version number do not change and are not exactly the same as the new pair, construct a new pair object and update the pair by CAS.

  • AtomicStampedReference uses version number to solve ABA

case

Use AtomicStampedReference to solve the ABA problem caused by AtomicInteger

public static void main(String[] args) {
    AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(1,1);
    int[] stampHolder = new int[1];

    new Thread(new Runnable() {
        @Override
        public void run() {
            int  value = atomicInteger.get(stampHolder);
            // Be sure to save the original value first
            int stamp = stampHolder[0];
            System.out.println("Thread 1 read value="+value+",stamp="+stampHolder[0]);
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 1 read value="+value+",stamp="+stampHolder[0]);
            // It is changed into stamp to pass in the previous value. If it is stampHolder[0], it is the latest value, which is meaningless
            if (atomicInteger.compareAndSet(value,3,stamp,stampHolder[0]+1)) {
                System.out.println("Thread 1 modification value from"+value+"To 3"+"stamp from"+stampHolder[0]+"reach"+(stampHolder[0]+1));
            }else{
                System.out.println("Thread 1 modification failed");
            }
        }
    }).start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            int  value = atomicInteger.get(stampHolder);
            System.out.println("Thread 2 read value="+value+",stamp="+stampHolder[0]);
            if (atomicInteger.compareAndSet(value,2,stampHolder[0],stampHolder[0]+1)) {
                System.out.println("Thread 2 modification value from"+value+"To 2"+"stamp from"+stampHolder[0]+"reach"+(stampHolder[0]+1));

                value = atomicInteger.get(stampHolder);
                System.out.println("Thread 2 read value="+value+",stamp="+stampHolder[0]);
                if (atomicInteger.compareAndSet(value,1,stampHolder[0],stampHolder[0]+1)){
                    System.out.println("Thread 2 modification value from"+value+"To 1"+"stamp from"+stampHolder[0]+"reach"+(stampHolder[0]+1));
                    value = atomicInteger.get(stampHolder);
                    System.out.println("Thread 2 read value="+value+",stamp="+stampHolder[0]);
                }
            }
        }
    }).start();
}

You can see that thread 1 failed to update 1 to 3 at last, because the version number also changed, and the ABA problem was successfully solved.

Thread 2 read value=1,stamp=1
 Thread 1 read value=1,stamp=1
 Thread 2 modification value From 1 to 2 stamp From 1 to 2
 Thread 2 read value=2,stamp=2
 Thread 2 modification value From 2 to 1 stamp From 2 to 3
 Thread 2 read value=1,stamp=3
 Thread 1 read value=1,stamp=3
 Thread 1 modification failed

summary

(1) The ABA problem should be paid attention to when using lock free structure in multi-threaded environment;

(2) The solution of ABA is generally controlled by the version number, and ensures that the data structure is transmitted by the element value, and each time an element is added, a new node is created to carry the element value;

(3) AtomicStampedReference uses Pair internally to store the element value and its version number;

Posted by timvw on Wed, 10 Nov 2021 16:24:02 -0800