Javaの原子類を一文で読む


一、ロックレス方式
Java並発注中の原子類はすべて無ロック方式に基づいて実現され、伝統的な反発ロックに比べて、無ロックはロック、ロック解除、スレッド切替の消費がないため、無ロックソリューションの性能はもっと良く、同時に無ロックはスレッドの安全を保証することができる.
1.ロックレス方式の実現原理
無ロックは主にCAS(Compare And Swap)に依存し、すなわち比較して交換し、CASはCPU命令であり、それ自体が原子性を保証することができる.CASには3つのパラメータがあります.
  • 共有変数のメモリアドレスA
  • 比較用の値B
  • 共有変数の新しい値C
  • public class SimpleCAS {
    
        private int value;
    
        public synchronized int cas(int expectVal, int newVal){
            int curVal = value;
            if (expectVal == curVal){
                value = newVal;
            }
            return curVal;
        }
    }
    

    上記のコードはCASの簡単な実装を示しており,メモリから現在のvalueの値を読み出し,期待値expectVal==curValのときにvalueを新しい値に更新すると判断する必要がある.
    CASベースの単純なスレッドセキュリティvalue+1法は、上記のコードのままで実現される.ここでcasメソッドは理解を助けるためにのみ使用されるので、実行結果に違いがある可能性があります.
    public class SimpleCAS {
    
        private volatile int value;
    
        public void addValue(){
            int newVal = value + 1;
            while (value != cas(value, newVal)){
                newVal = value + 1;
            }
        }
        
        private synchronized int cas(int expectVal, int newVal){
            int curVal = value;
            if (expectVal == curVal){
                value = newVal;
            }
            return curVal;
        }
    }
    

    スレッドはまずvalueの値を読み出して1を加算し、このとき別のスレッドがvalueを更新した場合、期待値とvalueが等しくなく、更新に失敗します.更新に失敗した後、ループ試行し、更新が正常にループを終了するまでvalueの値を再読み込みします.
    2.ABA問題
    ロックのない実装スキームで注意すべき問題の1つはABA問題である.
    例えば、上記のコードでは、valueの初期値は0であり、スレッドt 1はvalueの値を取り、それを1に更新し、スレッドはvalueを0に更新する.
    この過程で別のスレッドt 2があり、t 1と同時に初期値が0のvalueをとると、t 2はt 1の実行が完了した後にvalueを更新し、このときvalueは0であったが、t 1によって変更された.
    多くの場合、数値型データの加減などのABA問題に関心を持つ必要はありませんが、オブジェクト型のデータがABA問題に遭遇すると、前後の属性が変化している可能性があるので、解決する必要があります.
    解決策も簡単で、オブジェクトタイプのデータにバージョン番号を付けるとよい.更新するたびにバージョン番号に1を加えると、オブジェクトデータがAからBになってからAになっても、バージョン番号が増加し、オブジェクトが修正されたかどうかを見分けることができる.
    二、原子類
    1.原子化基本データ型
    3つの実装クラスがあります:AtomicBoolean、AtomicInteger、AtomicLong
    一般的な方法は、AtomicIntegerを例に挙げて、他の類似点です.
    AtomicInteger i = new AtomicInteger(0);
    
    i.getAndSet(int newValue);//          
    
    i.getAndIncrement();//    i ++
    i.incrementAndGet();//    ++ i
    
    i.getAndDecrement();//    i --
    i.decrementAndGet();//    -- i
    
    i.addAndGet(int delta);//    i + delta,        
    i.getAndAdd(int delta);//    i + delta,        
    
    i.compareAndSet(int expect, int update);//CAS   ,   boolean ,        
    
    i.getAndUpdate(update -> 10);//       
    i.updateAndGet(update -> 10);//    
    

    2.原子化オブジェクト参照タイプ
    実装クラスは,それぞれAtomicReference,AtomicStampedReference,AtomicMarkableReferenceであり,そのうち後者の2つはABA問題を解決するためのスキームを実現できる.
    AtomicReferenceでよく使われる方法は次のとおりです.
    //        Order   
    AtomicReference<Order> orderReference = new AtomicReference<>();
    
    orderReference.getAndSet(Order newValue);//     
    
    orderReference.set(Order order);//   
    Order order1 = orderReference.get();//    
    
    orderReference.compareAndSet(Order expect, Order update);//    
    
    orderReference.getAndUpdate();//       
    orderReference.updateAndGet();
    

    AtomicStampedReferenceでは、オブジェクトのバージョン番号(ABAの問題を解決するために使用される)に相当する初期値と初期stampを入力する必要があります.使用例は次のとおりです.
    AtomicStampedReference<String> reference = new AtomicStampedReference<>("roseduan", 0);
    
    //    stamp  
    boolean b = reference.attemptStamp(reference.getReference(), 10);
    
    //   
    String str = reference.getReference();
    
    //  stamp
    int stamp = reference.getStamp();
    
    //      stamp
    reference.set("I am not roseduan", 20);
    
    //    
    boolean b1 = reference.compareAndSet("roseduan", "jack", 20, 0);
    

    AtomicMarkableReferenceは、AtomicStampedReferenceのstampの代わりにmarkタグ(booleanタイプ)を使用し、ABA問題をより簡単に解決します.使用方法は上記と同様で、メソッドのstampをbooleanタイプの値に変更するだけでよい.
    3.原子化配列タイプ
    実装クラスは3つあります.
  • AtomicIntegerArray:原子化された整数配列
  • AtomicLongArray:原子化長整数配列
  • AtomicReferenceArray:原子化オブジェクト参照配列
  • 使用と原子化の基本タイプはあまり差がありませんが、方法に配列を付けるだけでいいです.
    4.原子化オブジェクト属性更新器
    3つの実装クラスもあります.
  • AtomicIntegerFieldUpdater:更新オブジェクトのフルサイズ属性
  • AtomicLongFieldUpdater:オブジェクトのロング整数属性を更新
  • AtomicReferenceFieldUpdater:オブジェクトの参照属性を更新
  • これら3つのクラスはいずれもJavaの反射メカニズムを用いて実現され,原子性を保証するために更新されるオブジェクトの属性はvolatileタイプでなければならない.使用例は次のとおりです.
    @Data
    @Builder
    public class User {
    
        private volatile int age;
    
        private volatile long number;
    
        private volatile String name;
    
        public static void main(String[] args) {
            User user = User.builder().age(22).number(15553663L).name("roseduan").build();
    
            //  age    
            AtomicIntegerFieldUpdater<User> integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
            integerFieldUpdater.set(user, 25);
    
            //  number    
            AtomicLongFieldUpdater<User> longFieldUpdater = AtomicLongFieldUpdater.newUpdater(User.class, "number");
            longFieldUpdater.set(user, 1000101L);
    
            //           
            AtomicReferenceFieldUpdater<User, String> referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name");
            referenceFieldUpdater.set(user, "I am not roseduan");
    
            System.out.println(user.toString());
        }
    }
    

    プログラムにUserクラスが作成され、3つの属性age、number、nameがそれぞれ整数、長整数、参照タイプに対応しています.次にオブジェクトプロパティ更新器を使用してプロパティ値の更新を行い、更新器の他の方法の使用は前述したいくつかの原子化タイプと似ています.
    5.原子化アキュムレータ
    実装クラスは4つあります.
  • DoubleAdder
  • DoubleAccumulator
  • LongAdder
  • LongAccumulator

  • これらのクラスの機能は限られており、累積操作のみを実行するために使用されますが、速度は非常に速いです.以下、DoubleAdderとDoubleAccumulatorの使い方を紹介します.残りの2つは似ています.
    //DoubleAccumulator    
    DoubleAccumulator a = new DoubleAccumulator(Double::sum, 0);//     0
    //  
    a.accumulate(1);
    a.accumulate(2);
    a.accumulate(3);
    a.accumulate(4);
    
    System.out.println(a.get());//  10
    
    //DoubleAdder    
    DoubleAdder adder = new DoubleAdder();
    adder.add(1);
    adder.add(2);
    adder.add(3);
    adder.add(4);
    adder.add(5);
    
    System.out.println(adder.intValue());//  15