web-dev-qa-db-ja.com

Java AtomicInteger compareAndSet()と同期キーワードのパフォーマンスはどうですか?

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キーワードの方が悪いと思います。

ありがとう!

27
Alan Kent

私の推測では、同期されたキーワードは、ロックをブロックするリスクがあるため(スレッドコンテキストスイッチなどを引き起こす可能性があるため)悪化しています。

はい、一般的なケースではあなたは正しいです。 実際のJava同時実行性 これについてはセクション15.3.2で説明しています。

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

AtomicPseudoRandomをロックと比較して不適切な選択として記述変数またはアトミック変数として非難する前に、図15.1の競合のレベルが非現実的に高いことを理解する必要があります。実際のプログラムはロックまたはアトミックを争うだけです。変数。実際には、アトミックは一般的な競合レベルをより効果的に処理するため、アトミックはロックよりもスケーリングが優れている傾向があります。

さまざまなレベルの競合でのロックとアトミック間のパフォーマンスの逆転は、それぞれの長所と短所を示しています。低から中程度の競合で、アトミックはより優れたスケーラビリティを提供します。競合が多い場合、ロックは競合の回避を向上させます。 (CASベースのアルゴリズムは、シングルCPUシステムのロックベースのアルゴリズムよりも優れています。これは、読み取り-変更-書き込み操作の途中でスレッドがプリエンプトされる可能性が低い場合を除いて、CASは常にシングルCPUシステムで成功するためです。 )

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

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

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

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

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

33
Péter Török

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

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

4
irreputable

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

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

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

  1. 簡単に始める
  2. プロフィール
  3. パフォーマンスが十分に良い場合は、単純な実装はそのままにしておきます
  4. パフォーマンスの向上が必要な場合は、賢くなり始め(おそらく最初はより特殊なロックを使用します)、[〜#〜] test [〜#〜][〜#〜] test [〜#〜][〜#〜] test [〜#〜]

ビジーウェイトロックのコードは次のとおりです。

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);
    }
}
0
Prof Mo