楽観的でロックフリー

ロックはAQS(AbstractQueuedSynchronizer)に基づいて実装されます。AQSはロックまたはその他の同期コンポーネントの基盤を構築するために使用されます。intメンバー変数を使用して、組み込みFIFOキューを介して状態(同期状態)を表し、リソース取得スレッドキュー。

同期モードでロックすると、スレッドがBLOCKED状態とRUNNABLE状態の間で切り替わります。オペレーティングシステムでは、ユーザーモードとカーネルモードの間で頻繁に切り替わるため、効率が比較的低くなります。

同期された実装とは異なり、AQSの多くのデータ構造の変更は、操作をCASに依存しており、CASは楽観的ロックの実装です。

場合

CASはCompareAnd Swapの略語で、比較と置換を意味します。

次の図に示すように、CASメカニズムは、メモリアドレスV、期待値E、および変更される新しい値Nの3つの基本オペランドを使用します。変数を更新する場合、変数の期待値EとメモリアドレスVの実際の値が同じ場合に限り、メモリアドレスVに対応する値がNに変更されます。
ここに画像の説明を挿入します
この変更が失敗した場合はどうすればよいですか?多くの場合、目的の値に変更されるまで再試行します。

例としてAtomicIntegerクラスを取り上げます。関連するコードは、次のとおりです。

public final boolean compareAndSet(int expectedValue, int newValue) {
    
    

        return U.compareAndSetInt(this, VALUE, expectedValue, newValue);

}

比較と置換は2つのアクションですが、CASはこれら2つの操作の原子性をどのように保証しますか?

追跡を続けたところ、jdk.internal.misc.Unsafeクラスによって実装されており、ループの再試行がここで発生したことがわかりました。

@HotSpotIntrinsicCandidate

public final int getAndAddInt(Object o, long offset, int delta) {
    
    

    int v;

    do {
    
    

        v = getIntVolatile(o, offset);

    } while (!weakCompareAndSetInt(o, offset, v, v + delta));

    return v;



}

JVMの内部をトレースし、Linuxマシンでos_cpu / linux_x86 /atomic_linux_x86.hppを参照します。ご覧のとおり、一番下の呼び出しはアセンブリ言語であり、最も重要なのはcmpxchgl命令です。CASのアトミック性は実際にはハードウェアCPUによって直接保証されているため、現時点では、コードを見つけるためにダウンする方法はありません。

template<>

template<typename T>

inline T Atomic::PlatformCmpxchg<4>::operator()(T exchange_value,

                                                T volatile* dest,

                                                T compare_value,

                                                atomic_memory_order /* order */) const {
    
    

  STATIC_ASSERT(4 == sizeof(T));

  __asm__ volatile ("lock cmpxchgl %1,(%3)"

                    : "=a" (exchange_value)

                    : "r" (exchange_value), "a" (compare_value), "r" (dest)

                    : "cc", "memory");

  return exchange_value;

}

CASによって実装されたアトミッククラスのパフォーマンスはどの程度向上しますか?共有変数を自動インクリメントするために20個のスレッドを開始しました。

テスト結果によると、頻繁な書き込み操作の場合、アトミッククラスのパフォーマンスは同期メソッドの3倍です。
ここに画像の説明を挿入します

楽観的ロック

上記の説明からわかるように、楽観的ロックは厳密にはロックではなく、競合を検出するメカニズムを提供し、競合がある場合は、操作を完了するために再試行メソッドを使用します。操作が再試行されない場合、楽観的ロックは単なる判断ロジックです。

ここから、楽観的ロックと悲観的ロックの違いを確認できます。悲観的なロックは、他の人がデータを操作するたびにそれを変更すると考えているため、他の誰かがロックを解放しない限り、データを操作するたびにロックを追加します。

楽観的ロックが競合を検出すると、複数回の再試行が発生するため、前に、楽観的ロックは読み取りが多く書き込みが少ないシナリオに適していると述べました。また、リソースの競合が深刻なシナリオでは、楽観的ロックは複数回失敗します。この状況では、 CPUのアイドリングが発生するため、このシナリオではペシミスティックロックのパフォーマンスが向上します。

読み取りを増やしたり書き込みを減らしたりするときに楽観的ロックを使用するのが適切なのはなぜですか?悲観的なロックを使用すると、読み取りを増やしたり書き込みを減らしたりするときに競合がほとんど発生しませんか?

実際、問題は競合の頻度ではなく、ロックの動作です。

ペシミスティックロックは、1つのロック、2つの読み取り、3つの更新の3つのモードに従う必要があります。競合がない場合でも、実行は非常に遅くなります。

前述のように、楽観的ロックは本質的にロックではなく、単なる判断ロジックであり、リソースの競合がほとんどない場合でもオーバーヘッドは発生しません。

上で説明したCAS操作は、楽観的ロックの典型的な実装です。ところで、CASの欠点、つまり楽観的ロックのいくつかの欠点を見てみましょう。

同時実行性が比較的高い場合、一部のスレッドは常に特定のリソースを変更しようとすることがありますが、深刻な競合のために更新が失敗しました。現時点では、CPUに大きな負荷がかかります。JDK 1.8で追加されたLongAdderは、元の値を分割し、最後にsumメソッドを使用することで、CAS操作の競合の可能性を減らします。パフォーマンスはAtomicLongの約10倍です。

CAS操作のオブジェクトは、単一のリソースのみにすることができます。複数のリソースのアトミック性を確保する場合は、同期などの従来のロック方法を使用するのが最適です。

ABA問題は、CAS操作中に、別のスレッドが変数の値をAからBに変更し、次にAに変更したことを意味します。現在のスレッドが操作中の場合、値はまだAであることが判明したため、交換します。オペレーティング。シナリオによっては、AtomicIntegerのように効果がないため、この状況にあまり注意を払う必要はありませんが、リンクリストなどの他の操作では問題が発生するため、回避する必要があります。AtomicStampedReferenceを使用して、原子性を確保するために整数バージョンスタンプで参照をマークできます。

楽観的ロックはバランスの更新を実現しますバランスの
操作は、取引システムで最も一般的な操作です。最初に残高の値を読み取り、いくつかの変更を加えてから、書き戻します。

天びんを更新するには、ロックが必要です。読み取り操作と書き込み操作はアトミックではないため、バランスの取れた複数の操作が同時に発生すると、不整合が発生します。

より明白な例を挙げてください。あなたは80元と5元の消費を同時に要求しました。手術後、両方の支払いは成功しましたが、最終的に残高は5元しか減少しませんでした。これは、5元で85元相当のものを購入するのと同じです。以下のタイミングをご覧ください。

リクエストA:残高を読む100

リクエストB:残高を読む100

リクエストA:5元を使い、一時的な残高は95です

リクエストB:80元を費やし、一時的な残高は20です

リクエストB:残高20の書き込みに成功しました

リクエストA:残高95の書き込みに成功しました

したがって、バランス操作をロックする必要があります。このプロセスはマルチスレッド操作に似ていますが、マルチスレッドは単一のマシンであり、バランスシナリオが分散されています。

データベースの場合、行ロックを追加することで解決できます。MySQLの場合、MyISAMは行ロックをサポートしていません。使用できるのはInnoDBのみです。一般的なSQLステートメントは次のとおりです。

select * from user where userid={id} for update

select for updateなどの単純なSQLを使用すると、実際には最下層に3つのロックが追加されますが、これは非常にコストがかかります。

主キーインデックスはデフォルトでロックされていますが、ここでは無視されます;二次インデックスuserid = {id}
の次のキーロック(レコード+ギャップロック);
二次インデックスuserid = {idの次のレコードのギャップロック}。

したがって、実際のシーンでは、この種の悲観的なロックは、最初は十分に普遍的ではないため、次に非常に高価であるため、使用されなくなりました。

より良い方法は、楽観的ロックを使用することです。上記の楽観的ロックの定義によれば、2つの概念を抽象化できます。

  1. 競合検出メカニズム:最初にこの操作のバランスEを見つけ、更新時に現在のデータベースの値と同じであるかどうかを判断し、同じである場合は更新アクションを実行します。

  2. 再試行戦略:競合がある場合は直接失敗するか、5回の再試行後に失敗します

擬似コードは次のとおりです。これが実際にはCASであることがわかります。

# old_balance获取

select balance from  user where userid={
    
    id}

# 更新动作 

update user set balance = balance - 20

    where userid={
    
    id} 

    and balance >= 20

    and balance = $old_balance

Redis分散ロック

Redisの分散ロックは、インターネット業界で頻繁に使用されるソリューションです。

  • ロックの作成:SETNX [KEY] [VALUE]アトミック操作。つまり、指定されたKEYが存在しない場合は、作成して1を返し、それ以外の場合は
    0を返します。通常、キー値の設定[EX秒] [PXミリ秒] [NX | XX]
    コマンドをより完全なパラメーターとともに使用し、同時にKEYのタイムアウトを設定します

  • ロッククエリ:キーが存在するかどうかを判断するだけで、キーを取得します

  • ロックの削除:DEL KEY、対応するKEYを削除するだけ

ネイティブセマンティクスによると、次の単純なロックおよびロック解除メソッドがあります。ロックメソッドは、一定の再試行によって分散ロックを取得し、削除コマンドによって分散ロックを破棄します。

public void lock(String key, int timeOutSecond) {
    
    

    for (; ; ) {
    
    

        boolean exist = redisTemplate.opsForValue().setIfAbsent(key, "", timeOutSecond, TimeUnit.SECONDS);

        if (exist) {
    
    

            break;

        }

    }

}

public void unlock(String key) {
    
    

    redisTemplate.delete(key);

}

このコードには多くの問題があり、最も深刻な問題の1つだけを指摘します。マルチスレッドでは、ロック解除メソッドは現在のスレッドでのみ実行できますが、上記の実装では、タイムアウトのためにロックが早期に解放されます。次の3つのリクエストのタイミングを考慮してください。

リクエストA:リソースxのロックを取得しました。ロックのタイムアウト期間は5秒です。

リクエストA:ビジネスの実行時間が比較的長いため、ビジネスはブロックされ、5秒以上待機しています

リクエストB:リクエストは6秒以内に開始され、ロックxが無効であることが判明したため、ロックは正常に取得されました。

リクエストA:7秒目にリクエストAが実行され、ロック解除アクションが実行されます。

リクエストC:リクエストCは、ロックが解放された直後にリクエストを開始しました。その結果、ロックリソースが正常に取得されました。

この時点で、リクエストBとリクエストCの両方がロックxを正常に取得し、分散ロックは無効です。ビジネスロジックを実行すると、問題が発生しやすくなります。

したがって、ロックを削除するときは、そのリクエスターが正しいかどうかを判断する必要があります。まず、ロック内の現在の識別子を取得し、次に削除するときに、この識別子がロック解除要求と同じであるかどうかを判断します。

読み取りと判断は2つの異なる操作であることがわかります。これら2つの操作の間にもギャップがあります。同時実行性が高いと、実行の問題が発生します。安全な解決策は、luaスクリプトを使用してそれらをアトムにカプセル化することです。

変更されたコードは次のとおりです。

public String lock(String key, int timeOutSecond) {
    
    

    for (; ; ) {
    
    

        String stamp = String.valueOf(System.nanoTime());

        boolean exist = redisTemplate.opsForValue().setIfAbsent(key, stamp, timeOutSecond, TimeUnit.SECONDS);

        if (exist) {
    
    

            return stamp;

        }

    }

}

public void unlock(String key, String stamp) {
    
    

    redisTemplate.execute(script, Arrays.asList(key), stamp);

}

local stamp = ARGV[1]

local key = KEYS[1]

local current = redis.call("GET",key)

if stamp == current then

    redis.call("DEL",key)

    return "OK"

end

ご覧のとおり、リードが分散ロックを実装することは依然として困難です。redlockのJavaクライアントを使用してredissonを実装することをお勧めします。これは、Redisによって公式に提案された分散ロック管理方法に従って実装されます。

このロックアルゴリズムは、複数のredisインスタンスシナリオおよびいくつかの異常な状況での分散ロックの問題を処理し、より高いフォールトトレランスを備えています。たとえば、前述のロックタイムアウトの問題では、ビジネスの正常な動作を保証するために、ウォッチドッグメカニズムを介してロックが無期限に更新されます。

redisson分散ロックの典型的な使用コードを見ることができます。

String resourceKey = "goodgirl";

RLock lock = redisson.getLock(resourceKey);

try {
    
    

    lock.lock(5, TimeUnit.SECONDS);

    //真正的业务

    Thread.sleep(100);

} catch (Exception ex) {
    
    

    ex.printStackTrace();

} finally {
    
    

    if (lock.isLocked()) {
    
    

        lock.unlock();

    }

}

redisのmonitorコマンドを使用すると、特定の実行ステップを確認できます。このプロセスはさらに複雑です。
ここに画像の説明を挿入します

ロックなし

ロックフリーとは、マルチスレッド環境で共有リソースにアクセスするときに、他のスレッドの実行をブロックしないことを指します。

Javaでは、ロックフリーキューの最も一般的な実装はConcurrentLinkedQueueですが、無制限であり、そのサイズを指定することはできません。ConcurrentLinkedQueueは、CASを使用してデータへの同時アクセスを処理します。これは、ロックフリーアルゴリズムを実現するための基礎です。

CAS命令は、コンテキストスイッチングやスレッドスケジューリングを引き起こさず、非常に軽量なマルチスレッド同期メカニズムです。また、エンキューやデキューなどのヘッドノードとテールノードでのいくつかのアトミック操作をより詳細なステップに分割し、CAS制御の範囲をさらに狭めます。

ConcurrentLinkedQueueは、高性能の非ブロッキングキューですが、あまり一般的には使用されていません。ブロッキングキューLinkedBlockingQueue(内部的にロックに基づく)と混同しないでください。

Disruptorは、ロックフリーの制限付きキューフレームワークであり、そのパフォーマンスは非常に高くなっています。RingBuffer、ロックフリー、キャッシュラインフィリングなどのテクノロジーを使用して、究極のパフォーマンスを追求します。非常に同時実行性のシナリオでは、従来のBlockingQueueの代わりに使用できます。

ログやメッセージなどの一部のミドルウェアでよく使用されますが(Stormはプロセスの内部通信メカニズムを実装するために使用します)、スパイクのようなシーンでない限り、ビジネスシステムで使用されることはめったにありません。そのプログラミングモデルはより複雑であり、ビジネスの主なボトルネックは、キューで遅くなるのではなく、主に遅いI / Oであるためです。

総括する

厳密に言えば、楽観的ロックはロックではありません。競合を検出するメカニズムを提供し、競合がある場合は、再試行方法を採用して操作を完了します。操作が再試行されない場合、楽観的ロックは単なる判断ロジックです。

悲観的なロックは、他の人がデータを操作するたびにそれを変更すると考えているため、他の誰かがロックを解放しない限り、データを操作するたびにロックを追加します。

読み取りが多く書き込みが少ない場合に楽観的ロックが悲観的ロックよりも高速である理由は、悲観的ロックには多くの追加操作が必要であり、楽観的ロックは競合なしにリソースをまったく消費しないためです。ただし、競合がより深刻な場合は常に再試行するため、ほとんどの場合、楽観的なロックは悲観的なロックよりも劣ります。

楽観的ロックのこの機能により、楽観的ロックは、より多くの読み取りとより少ない書き込みが利用できるインターネット環境で広く使用されています。

おすすめ

転載: blog.csdn.net/m0_50654102/article/details/114741265