Essential skills for Android Architects - operating CAS for concurrent programming

Keywords: Android

Principle of CAS

The full name of CAS is compare and swap. It is a mechanism for realizing synchronization function in multi-threaded environment. It is also lock free optimization, or spin, and adaptive spin.

In jdk, CAS adds volatile keyword as the cornerstone of implementation and contracting. There will be no contract without CAS. java.util.concurrent implements an optimistic lock different from synchronized with the help of CAS instructions.

A typical implementation mechanism of optimistic lock (CAS):

Optimistic locking consists of two steps:

  • Conflict detection
  • Data update

When multiple threads try to use CAS to update the same variable at the same time, only one thread can update the value of the variable, and other threads will fail. The failed thread will not hang, but will inform them that it has failed in the competition and can try again.

To ensure thread safety without using locks, there are three important operands in the CAS implementation mechanism:

  • Memory location to read and write
  • Expected original value (A)
  • New value

First read the memory location (V) to be read and written, and then compare the memory location (V) to be read and written with the expected original value (A). If the memory location matches the expected original value A, update the value of the memory location to the new value B. If the memory location does not match the expected original value, the processor will not do anything. In either case, it returns the value of the location before the CAS instruction.

It can be divided into three steps:

  • Read (memory location requiring read and write)
  • Comparison (memory location (V) to be read and written and expected original value (A))
  • Writeback (new value)

Three problems of CAS

Although CAS solves atomicity, it also has three major problems.

1) ABA problem

During CAS operation, it will check whether the value has changed. If there is no change, the update operation will be executed. If there is any change, the update operation will not be executed.

Assuming that the original value is a, then it is updated to B, and then it is updated to A. when you perform CAS inspection at this time, the value in memory is still a, you will mistakenly think that the value in memory has not changed, and then perform the update operation. In fact, the value in memory has changed at this time.

So how to solve the ABA problem? You can add a version number every time you update the value, then a - > b - > A will become 1A - > 2B - > 3a, and there will be no ABA problem at this time.

Starting with JDK 1.5, the AtomicStampedReference class is provided under the JUC package to solve the ABA problem. The compareandset () method of this class will first check whether the current reference is equal to the expected reference, and then check whether the current flags are equal to the expected flags. If they are equal, the casPair() method will be called to perform the update operation. The casPair() method finally calls the CAS method in the Unsafe class. The source code of compareAndSet() method of AtomicStampedReference class is as follows.

public boolean compareAndSet(V   expectedReference,
                                V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
   Pair<V> current = pair;
    // First check whether the expected reference is equal to the current reference and whether the expected ID is equal to the current ID
    // Then the CAS operation is executed (casPair() also calls the CAS method)
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

2) Performance issues

CAS will implement atomic operations in a circular way. If the long-time circular setting is not successful, it will always occupy the CPU, bring great execution overhead to the CPU and reduce the performance of the application.

3) Only atomic operations of one shared variable can be guaranteed

When performing CAS operations on a shared variable, atomic operations can be guaranteed, but when performing operations on multiple shared variables at the same time, CAS cannot guarantee the atomicity of these multiple shared variables at the same time. At this time, you can encapsulate multiple shared variables into an object, and then use the AtomicReference class provided under the JUC package to implement atomic operations. Another solution is to use locks.

Implement atomic operations

What is atomic operation

Atom means "particles that cannot be further divided", while atomic operation means "one or more series of operations that cannot be separated by the terminal". Suppose there are two operations A and B. If, from the perspective of the thread executing A, when another thread executes B, either all B is executed or B is not executed at all, then A and B are atomic to each other.

In java, atomic operations can be realized through locks and lock mechanisms, but sometimes more effective and flexible mechanisms are needed. The synchronized keyword is a blocking based locking mechanism, that is, when a thread has a lock, other threads accessing the same resource need to wait until the thread releases the lock, because the synchronized keyword is exclusive, If there are a large number of threads competing for resources, the CPU will spend a lot of time and resources to deal with these competitions, which will also cause deadlock. Moreover, the lock mechanism is equivalent to other lightweight requirements, which is a little too cumbersome, such as counters. Later, I will introduce the performance comparison between the two.

How to implement atomic operation

CAS can also be used to implement atomic operations. CMPXCHG instructions provided by the processor are used to implement atomic operations. Each CAS operation process includes three operators: A memory address V, an expected value A and A new value B. during operation, if the value stored on the address is equal to the expected value A, the value on the address is assigned to the new value B, Otherwise, do nothing.

The basic idea of CAS is to give a new value if the value on this address is equal to the expected value. Otherwise, nothing will be done, but the original value will be returned. Cyclic CAS is to do CAS operations continuously in a cycle until it is successful. The following code implements a CAS thread safe counter safeCount.

public class Counter {
    private AtomicInteger atomicCount = new AtomicInteger(0);
    private int i = 0;

    /** cas cafecount **/
    private void safeCount() {
        for (; ; ) {
            int i = atomicCount.get();
            boolean suc = atomicCount.compareAndSet(i, ++i);
            if (suc) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        Counter cas = new Counter();
        List<Thread> ts = new ArrayList<>(500);
        long start = System.currentTimeMillis();
        for (int j = 0; j < 100; j++) {
            Thread t = new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    cas.safeCount();
                }
            });
            ts.add(t);
        }
        for (Thread t : ts) {
            t.start();
        }
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(cas.i);
        System.out.println(cas.atomicCount.get());
        System.out.println(System.currentTimeMillis() - start);
    }
}

How does CAS implement thread security? The language level is not handled. We hand it over to the hardware CPU and memory. Using the multi-processing ability of the CPU, we can realize the blocking at the hardware level. In addition, the characteristics of volatile variables (visibility and ordering) can realize the thread safety based on atomic operation.

reference resources:
https://mp.weixin.qq.com/s/oe046IRUbYeXpIpYLz19ew
https://juejin.cn/post/6866795970274394126
https://blog.csdn.net/bugmiao/article/details/110859112

Posted by crazy/man on Sun, 19 Sep 2021 18:57:45 -0700