要約: 読み取りが多く書き込みが少ない環境において、ReadWriteLock より高速なロックはありますか? はい、それが JDK1.8 の新しい StampedLock です。
この記事は、Huawei Cloud Community「[高同時実行] 高同時実行シナリオにおける読み取り/書き込みロックよりも高速なロック」(著者: Binghe) から共有されたものです。
スタンプドロックとは何ですか?
ReadWriteLock ロックを使用すると、複数のスレッドが同時に共有変数を読み取ることができますが、共有変数を読み取るときに、他のスレッドが複数の共有変数を書き込むことはできなくなります。これは、読み取りが多く書き込みが少ない環境に適しています。では、読み取りが多く書き込みが少ない環境において、ReadWriteLock より高速なロックはあるのでしょうか?
答えはもちろんイエスです!それが今日紹介する主役、JDK1.8 の新しい StampedLock です。そうです、それです!
ReadWriteLock と比較して、StampedLock を使用すると、読み取り処理中に後続のスレッドが書き込みロックを取得して、共有変数に書き込むことができます。StampedLock を使用して共有変数を読み取る場合、読み取りデータの不整合を避けるために、共有変数が書き込まれています。受信検証操作であり、この読み取りは楽観的読み取りです。
つまり、StampedLock は、共有変数の読み取りプロセス中に後続のスレッドが書き込みロックを取得して共有変数に書き込むことを可能にするメソッドであり、オプティミスティック読み取りを使用してデータの不整合を回避し、読み取り回数が多く、読み取り回数が少ない同時実行性の高い環境で使用します。書き込み、ReadWriteLock より高速なロック。
StampedLockの3つのロックモード
ここでは、StampedLock と ReadWriteLock を単純に比較できます。ReadWriteLock は 2 つのロック モードをサポートしています: 1 つは読み取りロック、もう 1 つは書き込みロックです。ReadWriteLock を使用すると、複数のスレッドが同時に共有変数を読み取ることができます。読み取り中、書き込みは許可されません。書き込み時には読み取りは許可されず、読み取りと書き込みは相互に排他的であるため、ReadWriteLock の読み取りロックはむしろ悲観的読み取りロックを指します。
StampedLock は 3 つのロック モードをサポートしています:書き込みロック、読み取りロック(ここでの読み取りロックは悲観的読み取りロックを指します) 、および楽観的読み取り(多くの資料や書籍では楽観的読み取りロックと書かれていますが、ここでは個人的には楽観的読み取りの方が正確だと思います。なぜですか? 続けてみましょう)下を向いてください)。このうち、書き込みロックと読み取りロックのセマンティクスは ReadWriteLock に似ており、複数のスレッドが同時に読み取りロックを取得できますが、書き込みロックを取得できるのは 1 つのスレッドのみであり、書き込みロックと読み取りロックも相互排他的です。
ReadWriteLock とのもう 1 つの違いは、StampedLock が読み取りロックまたは書き込みロックを正常に取得した後、Long 型の変数を返し、ロックを解放するときにこの Long 型の変数を渡す必要があることです。たとえば、次の疑似コードに示すロジックは、StampedLock がロックを取得および解放する方法を示しています。
public class StampedLockDemo{
//创建StampedLock锁对象
public StampedLock stampedLock = new StampedLock();
//获取、释放读锁
public void testGetAndReleaseReadLock(){
long stamp = stampedLock.readLock();
try{
//执行获取读锁后的业务逻辑
}finally{
//释放锁
stampedLock.unlockRead(stamp);
}
}
//获取、释放写锁
public void testGetAndReleaseWriteLock(){
long stamp = stampedLock.writeLock();
try{
//执行获取写锁后的业务逻辑。
}finally{
//释放锁
stampedLock.unlockWrite(stamp);
}
}
}
StampedLock は、ReadWriteLock よりも優れたパフォーマンスの鍵となるオプティミスティック読み取りをサポートしています。 ReadWriteLock が共有変数を読み取ると、共有変数へのすべての書き込み操作がブロックされます。StampedLock によって提供されるオプティミスティック読み取りにより、複数のスレッドが共有変数を読み取るときに、1 つのスレッドが共有変数に書き込むことができます。
以下に示すように、JDK によって提供される公式の StampedLock の例を見てみましょう。
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
}
else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}
上記のコードでは、オプティミスティック読み取り操作の実行中に別のスレッドが共有変数に書き込むと、次のコード スニペットに示すように、オプティミスティック読み取りはペシミスティック読み取りロックにアップグレードされます。
double distanceFromOrigin() { // A read-only method
//乐观读
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
//判断是否有线程对变量进行了写操作
//如果有线程对共享变量进行了写操作
//则sl.validate(stamp)会返回false
if (!sl.validate(stamp)) {
//将乐观读升级为悲观读锁
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
//释放悲观锁
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
楽観的読み取りを悲観的読み取りロックにアップグレードするこの方法は、常に楽観的読み取りを使用するよりも合理的です。悲観的読み取りロックにアップグレードしない場合、プログラムは、楽観的読み取り操作がなくなるまで、ループ内で楽観的読み取り操作を繰り返し実行します。オプティミスティック読み取り操作。スレッドは書き込み操作を実行し、ループ内でオプティミスティック読み取りを実行すると、多くの CPU リソースが消費されます。これは、ペシミスティック読み取りロックにアップグレードするより合理的な方法です。
StampedLock の実装アイデア
StampedLock は CLH ロックに基づいて内部実装されており、CLH ロックは一種のスピン ロックであり、「飢餓現象」がないことを保証し、FIFO のサービス順序 (先入れ先出し) を保証します。
CLH では、ロックは待機中のスレッドのキューを維持します。ロックを申請しても成功しなかったすべてのスレッドは、このキューに格納されます。各ノードはスレッドを表し、現在のスレッドが解放されたかどうかを判断するためのフラグ (ロック済み) を保存します。 .Lock は、locked フラグが true の場合はロックが取得されたことを意味し、locked フラグが false の場合はロックが正常に解放されたことを意味します。
スレッドがロックを取得しようとすると、待機キューの末尾ノードを事前順序ノードとして取得し、次のようなコードを使用して、事前順序ノードがロックを正常に解放したかどうかを判断します。
while (pred.locked) {
//省略操作
}
プリオーダー ノード (pred) がロックを解放しない限り、現在のスレッドは実行を続行できないことを意味するため、スピンして待機します。そうでない場合、プリオーダー スレッドがロックを解放した場合、現在のスレッドはスレッドは実行を継続できます。
ロックが解放されるときもこのロジックに従い、スレッドは自身のノードのロック位置を false としてマークし、後続の待機スレッドは実行を続行できます。つまり、ロックは解放されます。
一般に、StampedLock の実装アイデアは比較的単純なので、ここでは説明しません。
StampedLockの注意事項
読み取りが多く書き込みが少ない高同時実行環境では、StampedLock は優れたパフォーマンスを発揮しますが、ReadWriteLock を完全に置き換えることはできません。使用する場合は、次の点にも特に注意する必要があります。
StampedLock は再入をサポートしていません
そうです、StampedLock は再入をサポートしていません。つまり、StampedLock を使用する場合はネストできないため、使用する場合は特別な注意を払う必要があります。
StampedLock は条件変数をサポートしていません
2 番目に注意すべきことは、StampedLock は条件変数をサポートしておらず、読み取りロックも書き込みロックも条件変数をサポートしていないことです。
StampedLock を不適切に使用すると、CPU のスパイクが発生する可能性があります
これは最も重要な点でもあり、使用するときは特に注意する必要があります。 スレッドが StampedLock の readLock() または writeLock() メソッドでブロックされている場合、ブロックされたスレッドの中断() メソッドを呼び出して、この時点でスレッドが実行されると、CPU が 100% に上昇します。たとえば、次のコードはそれを示しています。
public void testStampedLock() throws Exception{
final StampedLock lock = new StampedLock();
Thread thread01 = new Thread(()->{
// 获取写锁
lock.writeLock();
// 永远阻塞在此处,不释放写锁
LockSupport.park();
});
thread01.start();
// 保证thread01获取写锁
Thread.sleep(100);
Thread thread02 = new Thread(()->
//阻塞在悲观读锁
lock.readLock()
);
thread02.start();
// 保证T2阻塞在读锁
Thread.sleep(100);
//中断线程thread02
//会导致线程thread02所在CPU飙升
thread02.interrupt();
thread02.join();
}
上記のプログラムを実行すると、thread02 スレッドが配置されている CPU の使用率が 100% に上昇します。
ここで、多くの友人は、LockSupport.park(); によって thread01 が永久にブロックされる理由を理解していません。ここで、Glacier は、以下に示すように、スレッドのライフサイクル図を描画します。
もう分かりましたか?スレッドのライフサイクルには、説明する必要がある重要な状態がいくつかあります。
- NEW: 初期状態では、スレッドは構築されていますが、start() メソッドはまだ呼び出されていません。
- RUNNABLE: 実行可能状態。実行可能状態には、実行状態と準備完了状態が含まれます。
- BLOCKED: ブロックされた状態。この状態のスレッドは、他のスレッドがロックを解放するか、同期状態になるまで待つ必要があります。
- WAITING: 待機状態を示します。この状態のスレッドは、他のスレッドからの通知または割り込みを待ってから、次の状態に移行する必要があります。
- TIME_WAITING: タイムアウト待ち状態。一定時間になると勝手に戻ってくることもあります。
- TERMINATED: 終了状態。現在のスレッドは実行を終了しました。
このスレッドのライフサイクル図を読んだ後、LockSupport.park(); を呼び出すと thread02 がブロックされる理由がわかりますか?
したがって、StampedLock を使用する場合は、スレッドが配置されている CPU の高騰の問題を避けるように注意する必要があります。では、それを避けるにはどうすればよいでしょうか?
つまり、StampedLock の readLock() メソッドまたは読み取りロックと writeLock() メソッドを使用して書き込みロックを取得する場合、スレッドの割り込みメソッドを呼び出してスレッドを中断してはなりません。スレッドの場合、StampedLock の readLockInterruptibly() メソッドを使用して割り込み可能な読み取りロックを取得し、StampedLock の writeLockInterruptibly() メソッドを使用して割り込み可能なペシミスティック書き込みロックを取得する必要があります。
最後に、StampedLock の使用については、JDK 自体が提供する公式 StampedLock サンプルがベスト プラクティスです。友人は、JDK が提供する公式 StampedLock サンプルを見て、StampedLock の使用法と原則と中心的なアイデアについて詳しく学ぶことができます。その後ろに。。
クリックしてフォローして、Huawei Cloudの最新テクノロジーについて初めて学びましょう~