JAVAマルチスレッド(3)アトミック変数とノンブロッキング同期メカニズムの3番目の部分

コンカレントノートポータル:
1.0コンカレントプログラミング-マインドマップ
2.0コンカレントプログラミング-スレッドの安全性の基礎
3.0コンカレントプログラミング-基本的な構築モジュール
4.0コンカレントプログラミング-タスクの実行-将来
5.0コンカレントプログラミング-マルチスレッドのパフォーマンスとスケーラビリティ
6.0コンカレントプログラミング-明示的なロックと同期された
7.0同時プログラミング
-AbstractQueuedSynchronizer8.0同時プログラミング-アトミック変数と非ブロッキング同期メカニズム

SemaphoreおよびなどのJAVA同時パッケージの多くのクラスはConcurrentLinkedQueuesynchronizedメカニズムよりも高いパフォーマンスとスケーラビリティを提供しますこの性能向上の主な源である原子变量非阻塞同步机制アプリケーション。

ロックのデメリット

オーバーヘッドのスケジューリング

複数のスレッドがロックをめぐって競合する場合、JVMはオペレーティングシステムの機能を使用して一部のスレッドを一時停止し、後で実行を再開する必要があります。スレッドが実行を再開するとき、実行をスケジュールする前に、他のスレッドがタイムスライスを実行するのを待つ必要があります。スレッドの一時停止と再開のプロセスには多くのオーバーヘッドがあり、長期的な中断があります。

volatileの制限

volatileこれらの変数を使用すると、コンテキストの切り替えやスレッドスケジューリングなどの操作が発生しないため、変数はより軽量な同期メカニズムです。ただし、volatile可視性の保証は提供されていますが、アトミックコンプライアンス操作の構築には使用できません。

例:i++自己増加の問題。アトミック操作のように見えますが、実際には3つの独立した操作が含まれています。

  • 変数の現在の値を取得します
  • 値を1増やします
  • 新しい値を書く

これまでのところ、このアトミック操作を実現する唯一の方法はロックすることです。同じことが调度开销問題を引き起こす可能性があります。

ブロッキングの問題

スレッドがロックを待機しているときは、他に何もできません。ロックを保持しているときにスレッドが遅延すると、このロックを必要とするすべてのスレッドを実行できなくなります。

優先反転(優先反転)

マルチスレッドの競合中に、ブロックされたスレッドの優先度が高く、ロックを保持しているスレッドの優先度が低い場合、優先度の高いスレッドを最初に実行できたとしても、ロックが解放されるのを待つ必要があります。

同時実行のハードウェアサポート

同時マルチプロセッサの初期には、テストアンドセット、フェッチアンドインクリメント、スワップなどの特別な命令が提供されていました。今、ほぼすべての複数のプロセッサが原子の何らかの形で含まれている- -そのような比較およびスワップ(コンペアアンドスワップ)などの命令、関連するロード/ストア条件(ローディングリンク/ストア条件)。オペレーティングシステムとJVMは、これらの命令を使用して、ロックと同時データ構造を実装します。

排他的ロックは悲観的な技術であり、最悪の場合を想定しており、他のスレッドが干渉を引き起こさないようにする必要があります。

きめ細かい操作の場合、楽観的ロックは、干渉なしに更新操作を完了することができる、より効率的な方法です。この方法では、更新プロセス中に他のスレッドからの干渉があるかどうかを判断するための迅速な競合チェックメカニズムが必要です。干渉がある場合、この操作は失敗します。

CAS命令

ほとんどのプロセッサアーキテクチャでは、比較交換(CAS)命令が実装されます

CASには次の3つのオペランドが含まれています。

  • メモリ位置Vの読み取りと書き込みが必要
  • 比較する値A
  • 書き込まれる新しい値B

CASの意味は次のとおりです。位置Vの値はAである必要があると思います。そうである場合は、Vの値をBに更新します。そうしないと、変更されず、Vの値が実際に何であるかがわかります。

Java実装バージョン-非公式バージョン:

public class SimulatedCAS {
    
    
    private int value;
    public synchronized int get(){
    
    
        return value;
    }
    public synchronized int compareAndSwap(int expectValue,int newValue){
    
    
        int oldValue = value;
        if(oldValue == expectValue){
    
    
            value = newValue;
        }
        return oldValue;
    }

    public synchronized boolean compareAndSet(int expectValue,int newValue){
    
    
        return expectValue == compareAndSwap(expectValue, newValue);
    }
}

CASは楽観的なテクノロジーであり、更新操作を正常に実行することを望んでおり、別のスレッドが変数を変更した場合、CASはエラーを検出できます。

非常に便利なルールは次のとおりです。ほとんどのプロセッサでは、競合のないロックの取得と解放のための「高速コードパス」のコストは、CASの約2倍です。

JAVAロックとCAS

Java言語のロック構文は比較的簡潔ですが、JVMとロックを管理するときに完了する必要のあるタスクは単純ではありません。ロックを実装するときは、JVM内の非常に複雑なコードパスをトラバースする必要があり、オペレーティングシステムレベルのロック、スレッドの一時停止、コンテキストの切り替えなどが発生する可能性があります。CASの主な欠点は、呼び出し元が競合の問題(再試行、ロールバック、放棄)に積極的に対処する必要がある一方で、ロックが問題に自動的に対処できる(ブロッキング)ことです。

CASが失敗したときは何もしないのが賢明です。CASが失敗した場合、他のスレッドが実行したい操作を完了している可能性があることを意味します。

CASのJavaサポート

JAVA 5.0以降、数値タイプと参照タイプに効率的なCAS操作を提供するために、アトミック変数クラスが導入されました。下のjava.util.concurrent.atomicパッケージ(例:AtomicIntegerAtomicReferenceなど)

原子変数クラス

アトミック変数はロックよりも細かくて軽いため、複数のプロセッサで高性能の同時コードを実現することが重要です。
原子変数は一種の「より良いvolatile型変数」として使用できます。volatileアトミック更新操作のサポートに加えて、型変数と同じメモリセマンティクスを提供します

JAVA 5は、4つのに分かれて12アトミック変数クラス、追加标量类のグループを:更新器类数组类复合变量类、。

标量类 更新器类 数组类 复合变量类
AtomicBoolean AtomicIntegerFieldUpdater AtomicIntegerArray AtomicStampedReference
AtomicLong AtomicLongFieldUpdater AtomicLongArray AtomicMarkableReference
AtomicReference AtomicReferenceFieldUpdater AtomicReferenceArray
AtomicInteger

スレッドローカル計算の量が少ない場合、ロックとアトミック変数の競合は非常に激しくなります。
スレッドローカル計算の量が多い場合、ロックとアトミック変数の競合は減少します。

低から中レベルの競争では、原子変数はより高いスケーラビリティを提供できますが、高強度の競争では、ロックは効果的に競争を回避できます。

共有状態の使用を回避できれば、オーバーヘッドは小さくなります。競争の処理効率を向上させることでスケーラビリティを向上させることができますが、競争を完全に排除することによってのみ、真のスケーラビリティを実現できます。(これは本当に抽象的なものですが、サンプルコードから、次のThreadLocalカテゴリを理解できます)

ノンブロッキングアルゴリズム

特定のアルゴリズムでは、1つのスレッドの障害または一時停止によって他のスレッドの障害または一時停止が発生しない場合、このアルゴリズムは非ブロッキングアルゴリズムと呼ばれます。

非ブロッキングアルゴリズムは、スタック、キュー、優先キュー、ハッシュテーブルなど、多くの一般的なデータ構造で使用できます。

セキュリティカウンター-ノンブロッキングバージョン:

public class CasCounter {
    
    
    /**
     * 原子操作,线程安全。这是个假的 CAS 类,纯粹演示用哈
     */
    private SimulatedCAS simulatedCAS;
    /**
     * 非线程安全变量
     */
    private int temp;
    public CasCounter() {
    
    
        this.simulatedCAS = new SimulatedCAS();
    }
    public int get() {
    
    
        return simulatedCAS.get();
    }
    public int increment() {
    
    
        int value;
        do {
    
    
            value = simulatedCAS.get();
        } while (value != simulatedCAS.compareAndSwap(value, value + 1));
        return value + 1;
    }
    public void tempIncrement() {
    
    
        temp++;
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        CasCounter casCounter = new CasCounter();
        CountDownLatch count = new CountDownLatch(50);

        for (int i = 0; i < 50; i++) {
    
    
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    for (int j = 0; j < 30; j++) {
    
    
                        try {
    
    
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                        casCounter.increment();

                        try {
    
    
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                        casCounter.tempIncrement();
                    }
                    count.countDown();
                }
            }).start();
        }
        count.await();
        System.out.println("Thread safe final cas Counter : " + casCounter.get());
        System.out.println("Thread unsafe final temp value : " + casCounter.temp);
    }
}

ノンブロッキングスタック

非ブロッキングアルゴリズムを作成するための鍵は、データの一貫性を維持しながら、原子修飾の範囲を単一の変数に縮小する方法を理解することです。

スタックは最も単純な連鎖データ構造です。各要素は1つの要素のみを指し、各要素は1つの要素のみによって参照されます。

/**
 * 通过 AtomicReference 实现线程安全的入栈和出栈操作
 *
 * @param <E> 栈元素类型
 */
public class ConcurrentStack<E> {
    
    
    private final AtomicReference<Node<E>> top = new AtomicReference<>();

    /**
     * 将元素放入栈顶
     *
     * @param item 待放入的元素
     */
    public void push(E item) {
    
    
        Node<E> newHead = new Node<>(item);
        Node<E> oldHead = null;
        do {
    
    
            oldHead = top.get();
            newHead.next = oldHead;
        } while (!top.compareAndSet(oldHead, newHead));
    }

    /**
     * 弹出栈顶部元素
     *
     * @return 栈顶部元素,可能为 null
     */
    public E pop() {
    
    
        Node<E> oldHead;
        Node<E> newHead;
        do {
    
    
            oldHead = top.get();
            if (oldHead == null) {
    
    
                return null;
            }
            newHead = oldHead.next;
        } while (!top.compareAndSet(oldHead, newHead));
        return oldHead.item;
    }

    /**
     * 单向链表
     *
     * @param <E> 数据类型
     */
    private static class Node<E> {
    
    
        public final E item;
        public Node<E> next;

        public Node(E item) {
    
    
            this.item = item;
        }
    }
}

ノンブロッキングリンクリスト

リンクリストキューは、ヘッドポインタとテールポインタを別々に必要とするため、スタックよりも複雑です。新しい要素が正常に挿入されたら、両方のポインターをアトミック操作で更新する必要があります。

次の2つのスキルを理解する必要があります。

テクニック1

複数のステップを含む更新操作では、データ構造が一貫した状態にあることを確認してください。このように、Bスレッドが到着したときに、Aが更新を実行していることが判明した場合、Bスレッドは操作が部分的に完了したことを認識でき、すぐに独自の更新操作を開始することはできません。次に、BはAが更新されるまで(キューフラグを繰り返しチェックすることにより)待機できるため、2つのスレッドが互いに干渉することはありません。

テクニック2

Bが到着したときにAがデータ構造を変更していることをBが検出した場合、BがAの更新操作を完了できるように、データ構造に十分な情報があるはずです。BがAの更新操作の完了を「支援」する場合、BはAの操作が完了するのを待たずに自分の操作を実行できます。Aが回復後に他の操作を完了しようとすると、Bがその操作を完了したことがわかります。

例えば:

public class LinkedQueue<E> {
    
    
    /**
     * 链表结构
     * next 使用 AtomicReference 来管理,用来保证原子性和线程安全
     *
     * @param <E> 数据类型
     */
    private static class Node<E> {
    
    
        final E item;
        /**
         * 通过 AtomicReference 实现指针的原子操作
         */
        final AtomicReference<Node<E>> next;

        /**
         *  Node 构造方法
         * @param item 数据元素
         * @param next 下一个节点
         */
        public Node(E item, Node<E> next) {
    
    
            this.item = item;
            this.next = new AtomicReference<>(next);
        }
    }

    /**
     * 哨兵,队列为空时,头指针(head)和尾指针(tail)都指向此处
     */
    private final Node<E> GUARD = new Node<>(null, null);
    /**
     * 头节点,初始时指向 GUARD
     */
    private final AtomicReference<Node<E>> head = new AtomicReference<>(GUARD);
    /**
     * 尾节点,初始时指向 GUARD
     */
    private final AtomicReference<Node<E>> tail = new AtomicReference<>(GUARD);

    /**
     * 将数据元素放入链表尾部
     *
     * 在插入新元素之前,将首先检查tail 指针是否处于队列中间状态,
     * 如果是,那么说明有另一个线程正在插入元素。
     *      此时线程不会等待其他线程执行完成,而是帮助他完成操作,将 tail 指针指向下一个节点。
     *      然后重复进行检查确认,直到 tail 完全处于队列尾部才开始执行自己的插入操作。
     * 如果两个线程同时插入元素,curTail.next.compareAndSet 会失败,这种情况下不会对当前数据结构造成破坏。当前线程只需重新读取tail 并再次重试。
     * 如果curTail.next.compareAndSet执行成功,那么插入操作已生效。
     * 此时 tail.compareAndSet(curTail, newNode) 会进行尾部指针的移动:
     *      如果移动失败,那么当前线程将直接返回,不需要进行重试
     *      因为另一个线程在检查 tail 时候会帮助更新。
     *
     * @param item 数据元素
     * @return true 成功
     */
    public boolean put(E item) {
    
    
        Node<E> newNode = new Node<>(item, null);
        while (true) {
    
    
            Node<E> curTail = tail.get();
            Node<E> tailNext = curTail.next.get();
            //判断下尾部节点是否出现变动
            if (curTail == tail.get()) {
    
    
                //tailNext节点为空的话,说明当前 tail 节点是有效的
                if (tailNext == null) {
    
    
                    //将新节点设置成 当前尾节点 的 next节点,此处为原子操作,失败则 while 循环重试
                    //技巧1 实现点
                    if (curTail.next.compareAndSet(null, newNode)) {
    
    
                        //将 tail 节点的指针指向 新节点
                        //此处不用担心 tail.compareAndSet 会更新失败
                        //因为当更新失败的情况下,肯定存在其他线程在操作
                        //另一个线程会进入 tailNext!=null 的情况,重新更新指针
                        tail.compareAndSet(curTail, newNode);
                        return true;
                    }
                } else {
    
    
                    //当前尾节点 的 next 不为空的话,说明链表已经被其他线程操作过了
                    //直接将 tail 的 next 指针指向下个节点
                    //技巧2 实现点
                    tail.compareAndSet(curTail, tailNext);
                }
            }
        }
    }
}

最新のコードから判断すると、コンカレントパッケージの多くのツールクラスが変更および最適化されています。たとえば、内部実装はほとんどのコンカレントクラスの実現モードに変更されています。

        private static final sun.misc.Unsafe UNSAFE;
        private static final long itemOffset;
        private static final long nextOffset;

        static {
    
    
            try {
    
    
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> k = Node.class;
                itemOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("item"));
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
            } catch (Exception e) {
    
    
                throw new Error(e);
            }
        }
    }

ABAの問題

ABA問題のCAS操作は本当に頭痛の種AtomicStampedReferenceです。バージョン番号への参照を追加することでABA問題を回避するためにJavaが提供されています。同様にAtomicMarkableReference、ブール型を使用して、ABA問題を解決するためにノードが削除されているかどうかをマークします。

総括する

非ブロッキングアルゴリズムは、基盤となる同時実行プリミティブ(ロックの代わりにCASなど)を介してスレッドの安全性を維持します。これらの基礎となるプリミティブは、原子変数クラスを介して外界に公開されます。
ノンブロッキングアルゴリズムは、設計と実装が非常に困難ですが、通常、より高いスケーラビリティを提供します。JVMアップグレードプロセスでは、同時実行パフォーマンスの主な改善は、非ブロッキングアルゴリズムの使用によるものです(すでにプラットフォームライブラリにあるJVMで)。

おすすめ

転載: blog.csdn.net/lijie2664989/article/details/105739342