Java multi-threaded concurrency [7] CAS and atomic classes

Java multi-threaded concurrency [7] CAS and atomic classes

Continue to create, accelerate growth! This is the first day of my participation in the "Nuggets Daily New Plan · June Update Challenge", click to view the details of the event

CAS

CAS, full name compare and swap, compare and exchange, is a mechanism used to solve the performance loss caused by the use of locks in the case of multi-threaded concurrency. A CAS operation consists of three operands - the memory location (V), the expected old value (A), and the new value (B). If the value of the memory location matches the expected original value, the processor automatically updates the location value to the new value. Otherwise, the processor does nothing.

Looking back at the content of thread safety, there are three types of concurrent synchronization schemes for threads:

  • Mutual exclusion synchronization
  • non-blocking synchronization
  • No synchronization scheme

Mutual exclusion synchronization is the safest way to ensure thread safety, but its main problem is that thread blocking and waking up will bring performance problems, so mutual exclusion synchronization is also called blocking synchronization. From the point of view of dealing with problems, mutual exclusion synchronization is a pessimistic concurrency strategy: it is always believed that there will be problems without synchronization measures. Regardless of whether the data is competing, locks, user mode kernel mode switching, maintenance lock counters and Check if there are blocked threads that need to be woken up, etc.

The opposite is the optimistic concurrency strategy based on conflict detection: operate first, if no other threads compete for data, the operation succeeds; if other threads compete for data, take other remedial measures (retry). Most implementations of optimistic concurrency strategies do not need to suspend threads and are therefore called non-blocking synchronization.

The non-blocking synchronization implementation scheme in Java is the CAS operation. The JDK encapsulates the sun.misc.Unsafeclass to provide a fast CAS operation method, but it is not open to developers for direct use, but is indirectly used through some other APIs in JUC. The common ones are Atomic class.

AtomicInteger i = new AtomicInteger();
i.incrementAndGet();

// incrementAndGet 内部调用
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
复制代码

underlying principle

Taking the atomic class as the entry point, we go deep into the source code to see its underlying implementation:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
复制代码

Here unsafeis a jdk.internal.misc.Unsafetype:

private static final Unsafe unsafe = Unsafe.getUnsafe();
复制代码

View the source code in the getUnsafemethod :

private static final Unsafe theUnsafe = new Unsafe();
    
public static Unsafe getUnsafe() {
    return theUnsafe;
}
复制代码

It is equivalent to Unsafe creating a static variable by default for external getUnsafe()access .

AtomicInteger 中的 ++ 操作,调用了 Unsafe 的非静态方法 getAndAddInt(Object, long, int) ,这个方法的实现是:

@IntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}
复制代码

在这个方法中实现了一个典型的 CAS,通过 do..while 循环,不断去获取值,并且尝试进行修改。

首先简单看看如何获取值的:

/** Volatile version of {@link #getInt(Object, long)}  */
@IntrinsicCandidate
public native int getIntVolatile(Object o, long offset);
复制代码

看起来这是一个 native 方法,在 src/hotspot/share/opto/library_call.cpp 中找到了相关的内容:

case vmIntrinsics::_getIntVolatile: return inline_unsafe_access(!is_store, T_INT, Volatile, false);
复制代码

循环的终止条件是 !weakCompareAndSetInt(o, offset, v, v + delta),这个方法的定义是:

@IntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) {
    return compareAndSetInt(o, offset, expected, x);
}
复制代码

内部实际调用 compareAndSetInt

    @IntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
复制代码

compareAndSetInt 也是个 native 方法,对应 C 语言代码是逻辑是:

case vmIntrinsics::_compareAndSetInt:  return inline_unsafe_load_store(T_INT,    LS_cmp_swap,      Volatile);
复制代码
bool LibraryCallKit::inline_unsafe_load_store(const BasicType type, const LoadStoreKind kind, const AccessKind access_kind) {
  // ...
  switch (kind) {
    case LS_cmp_exchange: {
      result = access_atomic_cmpxchg_val_at(base, adr, adr_type, alias_idx, oldval, newval, value_type, type, decorators);
      break;
    }
    case LS_cmp_swap_weak:
      decorators |= C2_WEAK_CMPXCHG;
    case LS_cmp_swap: {
      result = access_atomic_cmpxchg_bool_at(base, adr, adr_type, alias_idx, oldval, newval, value_type, type, decorators);
      break;
    }
    case LS_get_set: {
      result = access_atomic_xchg_at(base, adr, adr_type, alias_idx, newval, value_type, type, decorators);
      break;
    }
    case LS_get_add: {
      result = access_atomic_add_at(base, adr, adr_type, alias_idx, newval, value_type, type, decorators);
      break;
    }
    default:
      ShouldNotReachHere();
  }
  assert(type2size[result->bottom_type()->basic_type()] == type2size[rtype], "result type should match");
  set_result(result);
  return true;
}
复制代码

忽略一些无用的细节,在这个方法最后根据 kind 去执行了不同的逻辑,而在`compareAndSetInt 方法中,我们传入的 kind 是 LS_cmp_swap :

    case LS_cmp_swap: {
      result = access_atomic_cmpxchg_bool_at(base, adr, adr_type, alias_idx, oldval, newval, value_type, type, decorators);
      break;
    }
复制代码

在这个分支中执行的是 access_atomic_cmpxchg_bool_at方法,方法名中的 cmpxchg 的含义是汇编指令,所以这个方法应该是 C 语言操作汇编指令的实现:

Node* GraphKit::access_atomic_cmpxchg_bool_at(Node* obj,
                                              Node* adr,
                                              const TypePtr* adr_type,
                                              int alias_idx,
                                              Node* expected_val,
                                              Node* new_val,
                                              const Type* value_type,
                                              BasicType bt,
                                              DecoratorSet decorators) {
  C2AccessValuePtr addr(adr, adr_type); //
  C2AtomicParseAccess access(this, decorators | C2_READ_ACCESS | C2_WRITE_ACCESS, bt, obj, addr, alias_idx);
  if (access.is_raw()) {
    return _barrier_set->BarrierSetC2::atomic_cmpxchg_bool_at(access, expected_val, new_val, value_type);
  } else {
    return _barrier_set->atomic_cmpxchg_bool_at(access, expected_val, new_val, value_type);
  }
}
复制代码

通过内存屏障来保证了操作的原子性和防止重排。

而这个终止条件的含义就是尝试更新值是否成功,如果失败就会一直去执行 do 代码块中的尝试获取值的操作。

而通过这个我们也可以看出,在 Java 中,JDK 为我们提供好了关于 CAS 能力的封装,就是原子类。

原子类

原子类底层封装了 CAS 的能力,常见的原子类基本类型包括:

AtomicBoolean
AtomicInteger
AtomicLong
复制代码

而用于操作数组的包括:

AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
复制代码

用于引用类型包括:

AtomicReference<V> // 原子更新引用类型。
AtomicMarkableReferce<E> // 原子更新带有标记位的引用类型。
AtomicStampedReference<V> // 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。
...
复制代码

用于更新字段的类有四种:

AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
AtomicReferenceFieldUpdater: 原子更新引用类型字段的更新器。
复制代码

原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。

基本类型的原子类实现

以 AtomicInteger 为例,它提供了一些常用的 API:

public final int get():获取当前的值
public final int getAndSet(int newValue):获取当前的值,并设置新的值
public final int getAndIncrement():获取当前的值,并自增
public final int getAndDecrement():获取当前的值,并自减
public final int getAndAdd(int delta):获取当前的值,并加上预期的值
void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
复制代码

相比普通的 int 类型,AtomicInteger 保障了操作的原子性。比如最常见的自增操作:

i++  // // 等效于 i = i + 1 ,三步原子操作:读取 i, 进行 +1 操作,赋值给 i 。
复制代码

When iis a normal int type, it i++is a three-step operation and cannot guarantee thread safety:

var temp = i
temp = temp + 1
i = temp
复制代码

While using the atomic class AtomicInteger, it provides an auto-increment method getAndIncrement(), and its internal logic:

public final int getAndIncrement() {
    return U.getAndAddInt(this, VALUE, 1);
}
复制代码

And the getAndAddIntmethod :

    @IntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }
复制代码

At the C language level, it is the compareAndSetIntsame as :

case vmIntrinsics::_getAndAddInt:    return inline_unsafe_load_store(T_INT,    LS_get_add,       Volatile);
复制代码

Its kind is LS_get_addthat the branch executed in the inline_unsafe_load_storemethod is:

    case LS_get_add: {
      result = access_atomic_add_at(base, adr, adr_type, alias_idx, newval, value_type, type, decorators);
      break;
    }
复制代码

It also implements CAS at the Java level, and the bottom layer implements atomic operations.

Disadvantages of CAS

Although CAS seems to be a very elegant solution, this operation does not cover all use cases of mutual exclusion synchronization, and CAS is not semantically perfect, it has a logical loophole:

ABA problem

If a variable V has the value A when it is first read, and it is checked that it is still the value A when it is ready to be assigned, can we say that its value has not been changed by other threads? If its value was ever changed to B during this period, and then changed back to A, the CAS operation would mistakenly believe that it was never changed.

In order to solve this ABA problem, the JUC package provides a marked atomic reference class AtomicStampedReference, which can ensure the correctness of CAS by controlling the version of the variable value, but most ABA problems do not affect the correctness of concurrent programs, so It has no substantial effect. If you need to solve the ABA problem, you still have to use the scheme in mutual exclusion synchronization.

AtomicStampedReference solves the ABA problem

AtomicStampedReference mainly maintains a pair object containing an object reference and an integer "stamp" that can be automatically updated to solve the ABA problem.

The solution is to mark each updated value, and record when we access the same value for the second time, compare whether the mark has been updated, and we can find out whether the value has changed.

public class AtomicStampedReference<V> {
    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;
    // ...
    
    /**
      * expectedReference :更新之前的原始值
      * newReference : 将要更新的新值
      * expectedStamp : 期待更新的标志版本
      * newStamp : 将要更新的标志版本
      */
    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) ||
            // 构造新的 Pair 对象并 CAS 更新
            casPair(current, Pair.of(newReference, newStamp)));
    }

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        // 调用 Unsafe 的 compareAndSwapObject() 方法 CAS 更新 pair 的引用为新引用
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }
}
复制代码

Guess you like

Origin juejin.im/post/7103889343957172238