27

リクエスト インスタンスの FIFO キュー (速度のために事前に割り当てられたリクエスト オブジェクト) を実装し、add メソッドで "synchronized" キーワードを使用することから始めました。メソッドは非常に短いものでした (固定サイズのバッファーに余裕があるかどうかを確認してから、配列に値を追加します)。visualVM を使用すると、スレッドが私が思っていたよりも頻繁にブロックしているように見えました (正確には「モニター」)。そこで、現在のサイズを追跡するなどの目的で AtomicInteger 値を使用するようにコードを変換し、while ループで compareAndSet() を使用します (AtomicInteger は、incrementAndGet() などのメソッドに対して内部的に行うように)。コードはかなり長く見えます。

私が疑問に思っていたのは、同期された短いコードを使用した場合と、同期されたキーワードを使用しない長いコードを使用した場合のパフォーマンスのオーバーヘッドはどうなるかということです (したがって、ロックでブロックすることはありません)。

以下は、synchronized キーワードを使用した古い get メソッドです。

public synchronized Request get()
{
    if (head == tail)
    {
        return null;
    }
    Request r = requests[head];
    head = (head + 1) % requests.length;
    return r;
}

以下は、synchronized キーワードを使用しない新しい get メソッドです。

public Request get()
{
    while (true)
    {
        int current = size.get();
        if (current <= 0)
        {
            return null;
        }
        if (size.compareAndSet(current, current - 1))
        {
            break;
        }
    }

    while (true)
    {
        int current = head.get();
        int nextHead = (current + 1) % requests.length;
        if (head.compareAndSet(current, nextHead))
        {
            return requests[current];
        }
    }
}

私の推測では、コードが短くても、ロックでブロックされるリスクがあるため (潜在的にスレッド コンテキスト スイッチなどを引き起こす可能性があります)、synchronized キーワードの方が悪いと思います。

ありがとう!

4

4 に答える 4

34

ロックでブロックされるリスクがあるため、同期キーワードの方が悪いと思います(スレッドコンテキストの切り替えなどを引き起こす可能性があります)

はい、一般的なケースでは、あなたは正しいです。Java Concurrency in Practiceでは、セクション 15.3.2 でこれについて説明しています。

[...] 競合レベルが高い場合、ロックはアトミック変数よりもパフォーマンスが優れている傾向がありますが、より現実的な競合レベルでは、アトミック変数がロックよりもパフォーマンスが優れています。これは、ロックが競合に反応してスレッドを一時停止し、共有メモリ バス上の CPU 使用率と同期トラフィックを削減するためです。(これは、プロデューサ/コンシューマ デザインでプロデューサをブロックすることでコンシューマの負荷が軽減され、コンシューマが追いつくのに似ています。) 一方、アトミック変数を使用すると、競合管理は呼び出し元のクラスにプッシュ バックされます。ほとんどの CAS ベースのアルゴリズムと同様AtomicPseudoRandomに、すぐに再試行することで競合に反応します。これは通常は正しいアプローチですが、競合の多い環境では競合が増えるだけです。

貧弱に書かれた変数やアトミック変数をロックに比べて悪い選択として非難する前にAtomicPseudoRandom、図 15.1 の競合のレベルが非現実的に高いことを認識しておく必要があります。実際のプログラムは、ロックまたはアトミック変数をめぐって競合するだけです。実際には、アトミックの方が典型的な競合レベルをより効果的に処理できるため、アトミックの方がロックよりもスケーリングしやすい傾向があります。

さまざまな競合レベルでのロックとアトミック間のパフォーマンスの逆転は、それぞれの長所と短所を示しています。低から中程度の競合で、アトミックはより優れたスケーラビリティを提供します。競合が多い場合、ロックはより優れた競合回避を提供します。(単一 CPU システムでは、CAS ベースのアルゴリズムはロックベースのアルゴリズムよりも優れています。 )

(テキストで参照されている数値では、図 15.1 は、競合が高い場合に AtomicInteger と ReentrantLock のパフォーマンスがほぼ同等であることを示しています。一方、図 15.2 は、競合が中程度の場合、前者が後者よりも 2 ~ 3 倍優れていることを示しています。 .)

更新: ノンブロッキング アルゴリズムについて

他の人が指摘しているように、ノンブロッキング アルゴリズムは高速になる可能性がありますが、より複雑であるため、正しく理解するのがより困難です。JCiA のセクション 15.4 からのヒント:

優れたノンブロッキング アルゴリズムは、スタック、キュー、プライオリティ キュー、ハッシュ テーブルなど、多くの一般的なデータ構造で知られていますが、新しいアルゴリズムの設計は専門家に任せるのが最善の作業です。

ノンブロッキング アルゴリズムは、ロックベースのアルゴリズムよりもかなり複雑です。ノンブロッキング アルゴリズムを作成するための鍵は、データの一貫性を維持しながら、アトミックな変更の範囲を 1 つの変数に制限する方法を理解することです。キューなどのリンクされたコレクション クラスでは、状態の変換を個々のリンクの変更として表現し、 を使用してAtomicReferenceアトミックに更新する必要がある各リンクを表すことで解決できる場合があります。

于 2010-08-24T12:19:40.843 に答える
4

スレッドを実際に中断する前に、jvm がすでにいくつかのスピンを行っているのではないかと思います。あなたのようなよく書かれた重要なセクションは非常に短く、ほとんどすぐに完了すると予想されます。したがって、スレッドをあきらめて一時停止する前に、何十ものループを楽観的にビジー待機する必要があります。その場合は、2 番目のバージョンと同じように動作するはずです。

プロファイラーが表示するものは、あらゆる種類のクレイジーな最適化により、フルスピードで jvm で実際に起こっていることとは大きく異なる場合があります。プロファイラーなしでスループットを測定して比較することをお勧めします。

于 2010-08-24T20:07:38.110 に答える
1

この種の同期の最適化を行う前に、絶対に必要であることをプロファイラーに伝える必要があります。

はい、一部の条件下での同期はアトミック操作よりも遅くなる可能性がありますが、元の方法と交換方法を比較してください。前者は非常に明確で保守が簡単ですが、後者は間違いなくより複雑です。このため、最初のテストでは見つからない非常に微妙な同時実行バグが存在する可能性があります。私はすでに 1 つの問題を認識しておりsizehead実際には同期が取れなくなる可能性があります。これらの操作はそれぞれアトミックですが、組み合わせはアトミックではなく、場合によっては一貫性のない状態になる可能性があるためです。

だから、私のアドバイス:

  1. シンプルに始める
  2. プロフィール
  3. パフォーマンスが十分に良い場合は、単純な実装をそのままにしておきます
  4. パフォーマンスの改善が必要な場合は、賢く始めて (最初はより特殊なロックを使用する可能性があります)、TESTTESTTEST
于 2010-08-24T12:38:41.943 に答える
0

ビジー待機ロックのコードを次に示します。

public class BusyWaitLock
{
    private static final boolean LOCK_VALUE = true;
    private static final boolean UNLOCK_VALUE = false;
    private final static Logger log = LoggerFactory.getLogger(BusyWaitLock.class);

    /**
     * @author Rod Moten
     *
     */
    public class BusyWaitLockException extends RuntimeException
    {

        /**
         * 
         */
        private static final long serialVersionUID = 1L;

        /**
         * @param message
         */
        public BusyWaitLockException(String message)
        {
            super(message);
        }



    }

    private AtomicBoolean lock = new AtomicBoolean(UNLOCK_VALUE);
    private final long maximumWaitTime ; 

    /**
     * Create a busy wait lock with that uses the default wait time of two minutes.
     */
    public BusyWaitLock()
    {
        this(1000 * 60 * 2); // default is two minutes)
    }

    /**
     * Create a busy wait lock with that uses the given value as the maximum wait time.
     * @param maximumWaitTime - a positive value that represents the maximum number of milliseconds that a thread will busy wait.
     */
    public BusyWaitLock(long maximumWaitTime)
    {
        if (maximumWaitTime < 1)
            throw new IllegalArgumentException (" Max wait time of " + maximumWaitTime + " is too low. It must be at least 1 millisecond.");
        this.maximumWaitTime = maximumWaitTime;
    }

    /**
     * 
     */
    public void lock ()
    {
        long startTime = System.currentTimeMillis();
        long lastLogTime = startTime;
        int logMessageCount = 0;
        while (lock.compareAndSet(UNLOCK_VALUE, LOCK_VALUE)) {
            long waitTime = System.currentTimeMillis() - startTime;
            if (waitTime - lastLogTime > 5000) {
                log.debug("Waiting for lock. Log message # {}", logMessageCount++);
                lastLogTime = waitTime;
            }
            if (waitTime > maximumWaitTime) {
                log.warn("Wait time of {} exceed maximum wait time of {}", waitTime, maximumWaitTime);
                throw new BusyWaitLockException ("Exceeded maximum wait time of " + maximumWaitTime + " ms.");
            }
        }
    }

    public void unlock ()
    {
        lock.set(UNLOCK_VALUE);
    }
}
于 2011-02-04T15:26:35.027 に答える