Javaマルチスレッド並行性[7]CASおよびアトミッククラス

Javaマルチスレッド並行性[7]CASおよびアトミッククラス

創造を続け、成長を加速させましょう!「ナゲッツデイリーニュープラン・6月アップデートチャレンジ」に参加した初日です。クリックしてイベントの詳細をご覧ください。

CAS

CAS、フルネームのコンペアアンドスワップ、コンペアアンドエクスチェンジは、マルチスレッド同時実行の場合にロックを使用することによって引き起こされるパフォーマンスの低下を解決するために使用されるメカニズムです。CAS演算は、メモリ位置(V)、予想される古い値(A)、および新しい値(B)の3つのオペランドで構成されます。メモリ位置の値が予想される元の値と一致する場合、プロセッサは位置値を新しい値に自動的に更新します。それ以外の場合、プロセッサは何もしません。

スレッドセーフの内容を振り返ると、スレッドの同時同期スキームには次の3つのタイプがあります。

  • 相互排除の同期
  • ノンブロッキング同期
  • 同期スキームなし

相互排除同期はスレッドセーフを確保するための最も安全な方法ですが、その主な問題は、スレッドのブロックとウェイクアップによってパフォーマンスの問題が発生することです。したがって、相互排除同期はブロッキング同期とも呼ばれます。問題に対処するという観点から、相互排除同期は悲観的な同時実行戦略です。同期手段がないと問題が発生すると常に考えられています。データが競合しているかどうかに関係なく、ロック、ユーザーモードカーネルモードの切り替え、メンテナンスロックカウンターとウェイクアップが必要なブロックされたスレッドがあるかどうかを確認します。

反対は、競合検出に基づく楽観的同時実行戦略です。最初に操作し、他のスレッドがデータを競合しない場合は操作が成功します。他のスレッドがデータを競合する場合は、他の修正手段を実行します(再試行します)。楽観的同時実行戦略のほとんどの実装は実行しません。スレッドを一時停止する必要があるため、非ブロッキング同期と呼ばれます。

Javaのノンブロッキング同期実装スキームはCAS操作です。JDKはsun.misc.Unsafeクラスて高速CAS操作メソッドを提供しますが、開発者が直接使用することはできませんが、JUCの他のAPIを介して間接的に使用されます。一般的なものはアトミッククラスです。

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

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

基本原則

アトミッククラスをエントリポイントとして、ソースコードを深く掘り下げて、その基礎となる実装を確認します。

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

タイプは次のとおりですunsafejdk.internal.misc.Unsafe

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

getUnsafeメソッドのソースコードを表示します:

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

これは、外部getUnsafe()アクセス。

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 。
复制代码

i通常のint型の場合、これi++は3段階の操作であり、スレッドの安全性を保証するものではありません。

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

AtomicIntegerそして、自動インクリメントメソッドを提供するアトミッククラスgetAndIncrement()とその内部ロジックを使用します。

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

そしてgetAndAddInt方法:

    @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;
    }
复制代码

C言語レベルでは、次compareAndSetIntと同じです。

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

その種類はLS_get_addinline_unsafe_load_storeメソッドが次のとおりです。

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

また、JavaレベルでCASを実装し、最下層はアトミック操作を実装します。

CASのデメリット

CASは非常に洗練されたソリューションのようですが、この操作は相互排除同期のすべてのユースケースを網羅しているわけではなく、CASは意味的に完全ではなく、論理的な抜け穴があります。

ABA問題

変数Vが最初に読み取られたときに値Aを持ち、割り当ての準備ができたときにまだ値Aであることが確認された場合、その値は他のスレッドによって変更されていないと言えますか?この期間中に値がBに変更され、その後Aに戻された場合、CASオペレーションは、値が変更されなかったと誤って信じてしまいます。

このABA問題を解決するために、JUCパッケージはマークされたアトミック参照クラスを提供しますAtomicStampedReference。これは変数値のバージョンを制御することでCASの正確性を保証できますが、ほとんどのABA問題は同時プログラムの正確性に影響を与えないため、実質的な影響はありません。ABA問題を解決する必要がある場合でも、相互排他同期でスキームを使用する必要があります。

AtomicStampedReferenceはABA問題を解決します

AtomicStampedReferenceは主に、オブジェクト参照と、ABA問題を解決するために自動的に更新できる整数の「スタンプ」を含むペアオブジェクトを維持します。

解決策は、更新された各値にマークを付け、同じ値に2回アクセスしたときに記録し、マークが更新されたかどうかを比較して、値が変更されたかどうかを確認することです。

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);
    }
}
复制代码

おすすめ

転載: juejin.im/post/7103889343957172238