ロックの紹介
Java マルチスレッド ロックは オブジェクト に基づいており、各オブジェクトをロックとして使用できます。クラス ロックはオブジェクト ロックでもあります
Java 6 以降、オブジェクトは 4 つのロック状態に分割され、低レベルから高レベルまで次のようになります。
- ロック解除状態
- バイアスロック状態
- 軽量ロック状態
- ヘビーウェイトロックステータス
Javaオブジェクトヘッダー
すべての Java オブジェクトにはオブジェクト ヘッダーがあります。オブジェクトヘッダは非配列型の場合は2ワード幅、配列型の場合は3ワード幅で格納されます。32 ビット プロセッサではワード幅は 32 ビットで、64 ビット仮想マシンではワード幅は 64 ビットです。オブジェクトヘッダーの内容は次のとおりです。
長さ | コンテンツ | 説明する |
---|---|---|
32/64ビット | マークワード | オブジェクトのhashCodeやロック情報などを格納します。 |
32/64ビット | クラスメタデータアドレス | オブジェクト型データへのポインタを格納する |
32/64ビット | 配列の長さ | 配列の長さ (配列の場合) |
主に Mark Word の形式を見てみましょう。
ロック状態 | 29ビットまたは61ビット | 1ビットは偏ったロックですか? | 2ビットロックフラグ |
---|---|---|---|
ロックなし | 0 | 01 | |
バイアスロック | スレッドID | 1 | 01 |
軽量ロック | スタック上のロック レコードへのポインタ | 現時点では、このビットはバイアスされたロックを識別するために使用されません。 | 00 |
重量級ロック | ミューテックスへのポインタ (ヘビーウェイト ロック) | 現時点では、このビットはバイアスされたロックを識別するために使用されません。 | 10 |
GCマーク | 現時点では、このビットはバイアスされたロックを識別するために使用されません。 | 11 |
Mark Word
オブジェクトのステータスがバイアスされたロックの場合、バイアスされたスレッド ID が格納され、ステータスが軽量ロックの場合、Mark Word
スレッド スタックへのポインタが格納されLock Record
、ステータスが重量ロックの場合、スレッドMark Word
スタックへのポインタが格納されることがわかります。ヒープが格納されている のモニター オブジェクトへのポインター。
1. バイアスロック:
Hotspot の作成者は、以前の研究で、ほとんどの場合、マルチスレッド間でロックの競合が存在しないだけでなく、常に同じスレッドによって複数回取得されるため、偏ったロックが導入されたことを発見しました。
バイアスされたロックは、そのロックにアクセスする最初のスレッドを優先します。以降の実行中に他のスレッドがロックにアクセスしない場合、バイアスされたロックを保持しているスレッドは同期をトリガーする必要はありません。つまり、リソースの競合がなく、CAS 操作も実行されない場合、バイアスされたロックにより同期ステートメントが削除され、プログラムの実行パフォーマンスが向上します。
変数をロックすることですが、これが真であれば、リソースの競合がないことを意味し、さまざまなロック/ロック解除のプロセスを経る必要がありません。false の場合は、他のスレッドがリソースを奪い合っていることを意味するため、以降の処理を実行します。
実装原則:
スレッドが初めて同期ブロックに入ると、ロック バイアスのスレッド ID がオブジェクト ヘッダーに格納され、ロック レコードがスタック フレームに格納されます。次回スレッドがこの同期ブロックに入るとき、スレッドは自身のスレッド ID がロックのマーク ワードに配置されているかどうかを確認します。
そうである場合、それはスレッドがロックを取得していることを意味し、スレッドは今後同期ブロックに出入りするときにロックとロック解除に CAS 操作を費やす必要がありません。そうでない場合は、別のスレッドがロックを取得していることを意味します。この偏ったロック。この時点で、CAS を使用して Mark Word のスレッド ID を新しいスレッドの ID に置き換えようとします。この時点では 2 つの状況が考えられます。
- 成功とは、以前のスレッドが存在しないことを意味します。Mark Word のスレッド ID は、新しいスレッドの ID です。ロックはアップグレードされず、バイアス ロックのままです。
- 失敗した場合は、前のスレッドがまだ存在していることを意味し、前のスレッドを一時停止し、バイアスされたロック フラグを 0 に設定し、ロック フラグを 00 に設定し、軽量ロックにアップグレードして、次の方法でロックを競合します。軽量ロック。
CAS:比較して交換します。
比較して設定します。ハードウェア レベルでアトミックな操作を提供するために使用されます。Intel プロセッサでは、比較と交換は cmpxchg 命令によって実装されます。指定した値と一致するか比較し、一致する場合は変更し、一致しない場合は変更しません。
プロセス:
2.軽量ロック
複数のスレッドが異なるタイミングで同じロックを取得します。つまり、ロックの競合がない場合、スレッドのブロックは発生しません。この状況に対応して、JVM は軽量ロックを使用して、スレッドのブロックとウェイクアップを回避します。
軽量ロック解除:
ロックが解放されると、現在のスレッドは CAS 操作を使用して、Displaced Mark Word の内容をロックされた Mark Word にコピーして戻します。競合が発生しない場合、コピー操作は成功します。複数のスピンにより他のスレッドが軽量ロックを重量ロックにアップグレードした場合、CAS 操作は失敗し、ロックが解放され、ブロックされたスレッドがウェイクアップされます。
3. ヘビーウェイトロック
重量ロックはオペレーティング システムの相互排他 (ミューテックス) に依存しており、オペレーティング システムのスレッド間の状態遷移には比較的長い時間がかかるため、重量ロックは非常に非効率的ですが、ブロックされたスレッドは CPU を消費しません。
前述したように、各オブジェクトはロックとみなすことができ、複数のスレッドが同時にオブジェクト ロックを要求すると、オブジェクト ロックは要求元のスレッドを区別するためにいくつかの状態を設定します。
Contention List:所有请求锁的线程将被首先放置到该竞争队列
Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner
!Owner:释放锁的线程
ロックのアップグレードプロセス
各スレッドが共有リソースを取得する準備をしているとき:
-
最初のステップは、独自の ThreadId が MarkWord に配置されているかどうかを確認することです。配置されている場合は、現在のスレッドが「偏ったロック」にあることを意味します。
-
2 番目のステップでは、MarkWord が独自の ThreadId でない場合、ロックがアップグレードされます。このとき、切り替えの実行には CAS が使用されます。新しいスレッドは、MarkWord の既存の ThreadId に従って前のスレッドに一時停止するように通知します。前のスレッドは MarkWord のコンテンツを空に設定します。
-
3 番目のステップでは、両方のスレッドがロック オブジェクトの HashCode をロックを保存するために新しく作成されたレコード スペースにコピーし、CAS 操作を通じてロック オブジェクトのマークワードの内容を新しく作成されたレコード スペースのアドレスに変更し始めます。 MarkWord と競合します。
-
4 番目のステップ 3 番目のステップでは、CAS の実行が成功してリソースを取得し、失敗した場合はスピンに入ります。
-
ステップ 5: 回転プロセス中に、回転スレッドがリソースを正常に取得すると (つまり、以前に取得したリソース スレッドが実行され、共有リソースが解放されます)、状態全体はまだ軽量ロック状態のままです。
-
6 番目のステップは、ヘビーウェイト ロック状態に入ることであり、この時点で、回転しているスレッドはブロックされ、前のスレッドが実行を完了して自身がウェイクアップするのを待ちます。
ロックの分類
1. 分類
1. リエントラントロックと非リエントラントロック
名前が示すように、いわゆるリエントラント ロックです。これは再入をサポートするロックです。つまり、このロックはリソースを繰り返しロックするスレッドをサポートします。
synchronized キーワードは、使用される再入可能ロックです。たとえば、同期インスタンス メソッド内でこのインスタンスの別の同期インスタンス メソッドを呼び出すと、例外なく再度ロックに入る可能性があります。
AQS を継承してシンクロナイザーを実装する場合、ロックを所有するスレッドが再度ロックを取得してスレッドがブロックされる可能性があるというシナリオは考慮されません。この場合、これは「非再入可能ロック」になります。
ReentrantLock
中国語の意味はリエントラントロックです。これは、この記事の後半で紹介する重要なクラスでもあります。
2. 公平なロックと不公平なロック
ここでいう「公平性」とは、実は一般的な意味での「早い者勝ち」、つまりFIFOのことを指します。ロックの場合、最初にロックを要求したスレッドが最初に満たされ、後でロックを要求したスレッドが後で満たされる場合、そのロックは公平です。そうでないと不公平になってしまいます。
通常の状況では、不公平なロックによって効率がある程度向上します。ただし、不当なロックはスレッドの枯渇を引き起こす可能性があります (一部のスレッドは長期間ロックを取得できません)。したがって、実際のニーズに応じて不公平なロックと公正なロックを選択する必要があります。
ReentrantLock は、不公平なロックと公正なロックをサポートします。
3. 読み書きロックと排他ロック
先ほど説明した同期ロックと ReentrantLock は、実際には「排他ロック」です。言い換えれば、これらのロックでは、同時に 1 つのスレッドのみがアクセスできます。
読み取り/書き込みロックにより、複数の読み取りスレッドが同時にアクセスできるようになります。Java は、読み取り/書き込みロックのデフォルト実装として ReentrantReadWriteLock クラスを提供します。このクラスは、読み取りロックと書き込みロックという 2 つのロックを内部的に維持します。読み取りロックと書き込みロックを分離することにより、「読み取りが多く書き込みが少ない」環境でのパフォーマンスが大幅に向上します。
読み取り/書き込みロックを使用しても、書き込みスレッドがアクセスすると、すべての読み取りスレッドと他の書き込みスレッドがブロックされることに注意してください。
同期だけでは、さまざまなビジネスのロック要件を満たすには程遠いことがわかります。次に、JDK のロックに関連するいくつかのインターフェイスとクラスを紹介します。
4. 楽観的ロックと悲観的ロック
-
楽観的ロック:
- オプティミスティック ロックは楽観的なアイデアです。つまり、読み取りが多くなり、書き込みが少なくなり、同時書き込みの可能性は低いと考えられます。データを取得するたびに、他の人がそのデータを変更しないと考えます。ロックされていますが、更新時にこの期間内にデータが更新されたかどうかを判断します
- 書き込み時は、まず現在のバージョン番号を読み出し、その後操作をロック(以前のバージョン番号と比較し、同じであれば更新)し、失敗した場合は読み取り、比較、書き込み操作を繰り返します。
- Java のオプティミスティック ロックは基本的に CAS 操作を通じて実装されます。CAS は、現在の値が受信値と同じかどうかを比較する更新されたアトミック操作です。同じであれば更新され、そうでなければ失敗します。
-
悲観的なロック:
- 悲観的ロックとは、悲観的な考え方です。つまり、書きすぎると同時書き込みが発生する可能性が高くなります。データを取得するたびに、他の人がそのデータを変更すると考え、データを取得するたびにデータをロックします。データの読み取りと書き込みを行うため、他のユーザーはロックを取得できません。このデータの読み取りと書き込みは、ロックが取得されるまでブロックされます。Java の悲観的ロックは同期されます。AQS フレームワークのロックは、最初に cas 楽観的ロックを試行してロックを取得しようとしますが、できません。それを入手してください。
RetreenLock などの悲観的なロックに変換されます
2. JDK のロックに関連するいくつかのインターフェイスとクラス
JDK の同時実行性に関するクラスのほとんどは、java.util.concurrent
1. 抽象クラス AQS/AQLS/AOS
AQS の「リソース」はint
データの種類で表されますが、業務で必要なリソースの数が範囲を超える場合があるためint
、JDK 1.6 では追加のAQLS (AbstractQueuedLongSynchronizer) が用意されています。コードは AQS とほぼ同じですが、リソースの種類がlong
type に変更されます。
AQS と AQLS は両方とも、 AOS (AbstractOwnableSynchronizer)と呼ばれるクラスを継承します。このクラスは JDK 1.6 にも登場しました。このクラスには、数行の単純なコードしかありません。ソース コード クラスのコメントから、ロックとホルダー (排他モード) の関係を表すために使用されていることがわかります。
2. インターフェース状態/ロック/ReadWriteLock
juc.locks パッケージの下には、Condition
、Lock
、の 3 つのインターフェイスがありますReadWriteLock
。このうち、Lock と ReadWriteLock は名前からわかり、それぞれロックと読み書きロックを意味します。Lock インターフェイスにはロックを取得および解放するためのメソッド宣言がいくつかありますが、ReadWriteLock には 2 つのメソッドしかなく、それぞれ「読み取りロック」と「書き込みロック」を返します。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
Lock インターフェースには、それを取得するためのメソッドがありますCondition
。
Condition newCondition();
各オブジェクトは、からObject
継承されたwait/notifyメソッドを使用して、待機/通知メカニズムを実装できます。Condition インターフェイスは、 Lockと連携して待機/通知モードを実装する、オブジェクト モニターと同様のメソッドも提供します。
では、Object の監視メソッドがあるのに、なぜ依然として Condition を使用する必要があるのでしょうか? 両者の簡単な比較は次のとおりです。
比較品 | オブジェクトモニター | 状態 |
---|---|---|
前提条件 | オブジェクトのロックを取得します | Lock.lock を呼び出してロックを取得し、Lock.newCondition を呼び出して Condition オブジェクトを取得します。 |
呼び出し方法 | object.notify() などの直接呼び出し | 直接呼び出し(condition.await() など) |
待機キューの数 | 1つ | 複数 |
現在のスレッドはロックを解放し、待機状態に入ります。 | サポート | サポート |
現在のスレッドはロックを解放し、待機状態を中断せずに待機状態に入ります。 | サポートしません | サポート |
現在のスレッドはロックを解放し、タイムアウト待ち状態に入ります。 | サポート | サポート |
現在のスレッドはロックを解放し、将来のある時点まで待機状態になります。 | サポートしません | サポート |
待機キュー内のスレッドを起動します | サポート | サポート |
待機キュー内のすべてのスレッドを起動します | サポート | サポート |
3、リエントラントロック
ReentrantLock は、Lock インターフェースの JDK デフォルト実装である非抽象クラスであり、ロックの基質機能を実現します。名前から判断すると「リエントラント」ロックですが、ソースコードから判断すると内部に抽象クラスがありSync
、AQSを継承して独自に実装したシンクロナイザです。同時に、ReentrantLock 内には 2 つの非抽象クラスがありNonfairSync
、FairSync
どちらも Sync を継承します。名前からもわかるように、「アンフェアシンクロナイザー」と「フェアシンクロナイザー」を意味します。これは、ReentrantLock が「公平なロック」と「不公平なロック」をサポートできることを意味します。
これら 2 つのシンクロナイザーのソース コードを見ると、それらの実装が「排他的」であることがわかります。AOS のすべてのメソッドが呼び出されるsetExclusiveOwnerThread
ため、ReentrantLock のロックは「排他的」です。つまり、そのロックはすべて「排他的ロック」であり、共有できません。
ReentrantLock の構築メソッドでは、boolean
型パラメータを渡して、デフォルトでは不公平である公平なロックかどうかを指定できます。このパラメータはインスタンス化されると変更できず、isFair()
メソッドを通じてのみ表示できます。
4、リエントラント読み取り書き込みロック
このクラスも非抽象クラスであり、** ReadWriteLock インターフェイスの JDK デフォルト実装です。**ReentrantLock と同様の機能を持ち、リエントラントであり、不公平なロックと公平なロックをサポートします。違いは、「読み取り/書き込みロック」もサポートしていることです。
ReentrantReadWriteLock は読み取り/書き込みロックを実装しますが、小さな欠点があります。つまり、「書き込み」操作中、他のスレッドは書き込みまたは読み取りができなくなります。この現象を「書き込み不足」と呼びます。この問題については、後ほど StampedLock クラスで引き続き説明します。
5、スタンプロック
StampedLock
クラスは Java 8 でのみリリースされました。Lock インターフェイスと ReadWriteLock インターフェイスは実装されていませんが、実際には「読み書きロック」機能が実装されており、そのパフォーマンスは ReentrantReadWriteLock よりも優れています。また、StampedLock は読み取りロックを「楽観的読み取りロック」と「悲観的読み取りロック」の 2 つのタイプに分類します。
前述したように、ReentrantReadWriteLock では「書き込み飢餓」が発生しますが、StampedLock では発生しません。それはどのようにして行われるのでしょうか?その中心的な考え方は、読み取り中に書き込みが発生した場合、書き込み操作をブロックするのではなく、再試行して新しい値を取得する必要があるということです。このモードも典型的なロックフリー プログラミングのアイデアであり、CAS スピンのアイデアと同じです。この操作方法により、StampedLock は読み取りスレッドが多く、書き込みスレッドが非常に少ないシナリオに非常に適していることがわかり、書き込み不足も回避されます。