十、Javaにおける原子操作クラス

7611 ワード

転載:『Java同時プログラミングの芸術』第7章
プログラムが変数を更新する場合、マルチスレッドが同時にこの変数を更新すると、変数i=1、Aスレッドがi+1、Bスレッドもi+1を更新するなど、所望以外の値が得られる可能性があり、2つのスレッド操作後、iは3ではなく2に等しい可能性がある.AとBスレッドは変数iを更新するときに得られるiがすべて1であるため、これがスレッドの安全ではない更新操作であり、通常synchronizedを使用してこの問題を解決し、synchronizedはマルチスレッドが同時に変数iを更新しないことを保証する.
JavaはJDK 1.5からjava.util.concurrent.atomicパッケージ(以下、Atomicパッケージと略称する)を提供し、このパッケージの原子操作クラスは、使用が簡単で、性能が効率的で、スレッドが安全に変数を更新する方法を提供する.
変数のタイプはいろいろあるので、Atomicパッケージにも多くのクラスが用意されており、ほぼ4種類のタイプの原子更新方式に属することができ、それぞれ原子更新基本タイプ、原子更新配列、原子更新参照、原子更新属性(フィールド)である.Atomicパッケージ内のクラスは基本的にUnsafeで実装されたパッケージクラスである.
1.原子更新基本タイプ
原子を用いて基本タイプを更新し,Atomicパケットは以下の3つのクラスを提供する.
  • AtomicBoolean:原子更新ブール型.
  • AtomicInteger:原子更新型.
  • AtomicLong:原子更新Longタイプ.

  • 以上の3つのクラスが提供する方法はほぼ同じであるため、本節ではAtomicIntegerを例に説明するだけで、AtomicIntegerの一般的な方法は以下の通りである.
  • int addAndGet(int delta):入力した値をインスタンスの値(AtomicIntegerのvalue)に原子的に加算し、結果を返します.
  • boolean compareAndSet(int expect,int update):入力された値が予想値に等しい場合、その値は原子的に入力された値に設定されます.
  • int getAndIncrement():現在の値を原子的に1加算します.ここでは、自己増加前の値を返します.
  • int getAndSet(int newValue):newValueの値を原子的に設定し、古い値を返します.
  • void lazySet(int newValue):最終的にはnewValueに設定され、lazySetを使用して値を設定すると、他のスレッドがその後しばらく古い値を読むことができる可能性があります.この方法の詳細については、同時プログラミングネットワーク翻訳の「AtomicLong.lazySetはどのように動作していますか?」を参照してください.
  • int getAndSet(int newValue):newValueの値を原子的に設定し、古い値
  • を返します.
    AtomicIntegerの例:
    AtomicInteger atomicInteger = new AtomicInteger();
    System.out.println(atomicInteger.getAndIncrement());
    

    ではgetAndIncrementはどのように原子操作を実現しているのでしょうか.その実現原理を一緒に分析してみましょう.getAndIncrementのソースコードは以下の通りです.
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
        return var5;
    }
    

    まずgetAndIncrementの方法を見てみましょう.それはunsafe類の簡単な包装にすぎません.具体的な論理はunsafeの中にあります.getAndAddIntメソッドを見てみると、このメソッドはまずthis.getIntVolatile(var 1,var 2)を通過する.var 1が指すメモリアドレスのvar 2の値を取得し、ここでは古い値を取得する.その後CASによりvar 5+var 4の値が更新され,更新に失敗してループに入る.
    Atomicパッケージは3つの基本タイプの原子更新を提供していますが、Javaの基本タイプにはchar、float、doubleなどがあります.では問題が来て、どのように原子の更新の他の基本的なタイプですか?Atomicパッケージのクラスは基本的にUnsafeで実現されていますが、Unsafeのソースコードを見てみましょう.
    /**
    *        expected,     Java     x
    * @return          true
    */
    public final native boolean compareAndSwapObject(Object o,long offset , Object expected , Object x );
    public final native boolean compareAndSwapInt(Object o , long offset , int expected, int x );
    public final native boolean compareAndSwapLong(Object o , long offset , long expected ,long x );
    

    コードにより,Unsafeは3つのCAS法しか提供していないことが分かった:compareAndSwapObject,compareAndSwapInt,compareAndSwapLong,AtomicBooleanソースコードを見ると,Booleanを整数に変換してからcompareAndSwapIntを用いてCASを行うので,原子更新char,float,double変数も同様の考え方で実現できることが分かった.
    // AtomicBoolean
    public final boolean compareAndSet(boolean expect, boolean update) {
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }
    
    // AtomicDouble
    public final boolean compareAndSet(double expect, double update) {
        return updater.compareAndSet(this,Double.doubleToRawLongBits(expect),Double.doubleToRawLongBits(update));
    }
    

    2.原子更新配列
    配列内の要素を原子で更新し、Atomicパッケージには4つのクラスがあります.
  • AtomicIntegerArray:原子更新型配列内の要素.
  • AtomicLongArray:原子更新長整数配列の要素.
  • AtomicReferenceArray:参照タイプ配列内の要素を原子更新します.

  • 上記のいくつかのクラスが提供する方法はほぼ同じで、AtomicIntegerArrayクラスを例に説明します.その一般的な方法は以下の通りです.
  • int addAndGet(int i,int delta):入力値を配列内のインデックスiの要素に原子的に加算します.
  • boolean compareAndSet(int i,int expect,int update):現在の値が予想値に等しい場合、配列位置iの要素を原子的にupdate値に設定します.
  • static int[] value = new int[] { 1, 2 };
    
    static AtomicIntegerArray ai = new AtomicIntegerArray(value);
    
    public static void main(String[] args) {
    
            ai.getAndSet(0,3);
    
            System.out.println(ai.get(0));
            System.out.println(value[0]);
    }
    

    出力結果:
    3
    1
    

    配列valueは構造方法によって伝達され、AtomicIntergerArrayは現在の配列をコピーするので、AtomicIntergerArrayが内部の配列要素を変更すると、伝達された配列には影響しません.
    構築方法:
    /**
     * Creates a new AtomicIntegerArray with the same length as, and
     * all elements copied from, the given array.
     *
     * @param array the array to copy elements from
     * @throws NullPointerException if array is null
     */
    public AtomicIntegerArray(int[] array) {
        // Visibility guaranteed by final field guarantees
        this.array = array.clone();
    }
    

    3.原子更新参照タイプ
    原子更新基本タイプのAtomicIntergerは、1つの変数しか更新できません.複数の変数を原子で更新する場合は、この原子を使用して参照タイプが提供するクラスを更新する必要があります.Atomicパッケージには、次の3つのクラスがあります.
  • AtomicReference:原子更新参照タイプ
  • AtomicReferenceFieldUpdater:原子更新参照タイプのフィールド.
  • AtomicMarkableReference:原子更新タグビット付き参照タイプ.ブールタイプのタグビットと参照タイプを原子的に更新できます.構造方法はAtomicMarkableReference(V initialRef,boolean initialMark)である.

  • 以上のクラスで提供されている方法はほとんど同じですので、ここではAtomicReferenceを例に説明します.AtomicReferenceの使用例コードは以下の通りです.
    public static AtomicReference atomicUserRef = new
            AtomicReference();
    
    public static void main(String[] args) {
        User user = new User("conan", 15);
        atomicUserRef.set(user);
        User updateUser = new User("Shinichi", 17);
        atomicUserRef.compareAndSet(user, updateUser);
        System.out.println(atomicUserRef.get().getName());
        System.out.println(atomicUserRef.get().getOld());
    }
    static class User {
        private String name;
        private int old;
        public User(String name, int old) {
            this.name = name;
            this.old = old;
        }
        public String getName() {
            return name;
        }
        public int getOld() {
            return old;
        }
    }
    
    //     
    Shinichi
    17
    

    その実現原理はunsafe.compareAndSwapObject法に依存した.
    public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }
    

    4.原子更新フィールドクラス
    クラス内のフィールドを原子的に更新する必要がある場合は、原子更新フィールドクラスを使用する必要があります.Atomicパッケージには、次の3つのクラスが原子フィールド更新を行います.
  • AtomicIntegerFieldUpdater:原子更新型のフィールドの更新器.
  • AtomicLongFieldUpdater:原子更新長整数フィールドの更新器.
  • AtomicStampedReference:原子更新バージョン番号付き参照タイプ.このクラスは整数値を参照に関連付け、原子の更新データとデータのバージョン番号に使用でき、CASを使用して原子の更新を行う場合に発生するABAの問題を解決することができます.

  • フィールドクラスを原子的に更新するには2つのステップが必要です.最初のステップは、原子更新フィールドクラスが抽象クラスであるため、使用するたびに静的メソッドnewUpdater()を使用して更新器を作成し、更新するクラスと属性を設定する必要があります.第2のステップでは、クラスのフィールド(プロパティ)を更新するにはpublic volatile修飾子を使用する必要があります.
    以上の3つのクラスで提供されている方法はほぼ同じですが、ここではAstomicIntegerFieldUpdaterを例に挙げて説明します.AstomicIntegerFieldUpdaterの例コードは以下の通りです.
    //        ,                 
    private static AtomicIntegerFieldUpdater a = AtomicIntegerFieldUpdater.
            newUpdater(User.class, "old");
    public static void main(String[] args) {
        //         10 
        User conan = new User("conan", 10);
        //       ,           
        System.out.println(a.getAndIncrement(conan));
        //          
        System.out.println(a.get(conan));
    }
    public static class User {
        private String name;
        public volatile int old;
        public User(String name, int old) {
            this.name = name;
            this.old = old;
        }
        public String getName() {
            return name;
        }
        public int getOld() {
            return old;
        }
    }
    

    実行結果は次のとおりです.
    10
    11