マルチスレッドを使用する主な目的は、システム リソースの使用率を向上させることです。ただし、複数のスレッドが同時に実行され、システム スケジューリングがランダムであるため、マルチスレッド環境でのビジネスでは、通常、同じコードを実行するように複数のスレッドが構成されます。このコード内に共有変数やいくつかの組み合わせ操作がある場合、スレッド間でのデータの混乱などのセキュリティ上の問題が必然的に発生します。
1スレッドセーフ
複数のスレッドが特定のクラス (オブジェクトまたはメソッド) にアクセスする場合、このクラスは常に正しい動作を示すことができ、このクラス (オブジェクトまたはメソッド) はスレッドセーフになります。
この時点で、スレッドの安全性を確保するためにさまざまなロック機構を使用する必要があり、プログラムは希望どおりに実行されます。
Java マルチスレッド ロックには主に 2 つのタイプがあります。
1、同期;
2. 明示的なロック。
二 synchronized
Synchronized は Java のキーワードであり、これを変更したコードがデータベースのミューテックスに相当します。コード ブロックをロックし、同時にメソッドまたは同期ブロック内にスレッドが 1 つだけ存在するようにし、変数へのスレッドのアクセスの可視性と排他性を確保できます。ロックを取得したオブジェクトは、コードの終了後にロックを解放します。
synchronized は、一度に 1 つのスレッドのみがロックされたメソッド ブロックに入ることができるミューテックスです。
2.1 機能
これには 3 つの主な機能があります。
1. 原子性: コード ブロックに同時にアクセスできるスレッドは 1 つだけです。
2. 秩序性: 同じロック操作の場合、ロックの前にロック解除が行われなければなりません。
3. 可視性: 同じ共有変数の変更は、他のスレッドからも可視化されます。
2.2 使用シナリオ
public class Synchronized {
// 修饰代码块
public void test(int i) {
Long test = 0L;
synchronized (test) {
i++;
}
}
// 修饰普通方法
public synchronized void test1() {
System.out.println();
}
// 修饰静态方法
public synchronized static void test2() {
System.out.println();
}
// 修饰静态全局变量
private static Long i;
public void test3() {
synchronized (i) {
System.out.println();
}
}
}
1.通常のメソッドまたはコード ブロックの変更: このシナリオでは、メソッドを呼び出しているオブジェクト (つまり、現在のクラスの現在のインスタンス オブジェクト) を実際にロックします。これは、「オブジェクト ロック」または「メソッド ロック」と呼ばれます。
2.静的メソッドまたは静的変数の変更: このシナリオでは実際に現在のクラス自体 (インスタンス オブジェクトではなく、クラスのバイトコード ファイル オブジェクト) をロックします。これは「クラス ロック」と呼ばれます。
ここで、クラス ロックを取得するスレッドは、オブジェクト ロックを取得するスレッドと競合しないことに注意してください。次に例を示します。
public class Synchronized {
public synchronized static void staticLock() {
for (int i = 0; i < 10; i++) {
System.out.println("staticLock execute ----- " + i);
}
}
public void normalMethod() {
for (int i = 0; i < 10; i++) {
System.out.println("nromalMethod execute ----- " + i);
}
}
public static void main(String[] args) {
Synchronized demo = new Synchronized();
Thread t1 = new Thread(() -> {
demo.normalMethod();
});
Thread t2 = new Thread(() -> {
staticLock();
});
t1.start();
t2.start();
}
}
出力結果:
2.3 同期原理
Java のすべてのオブジェクトには組み込みロック (モニター ロック モニター) があり、synchronized はオブジェクトの組み込みロックを使用してオブジェクトをロックします。
2.3.1 同期されたコードブロック
また、最初に例を示します。
public class Synchronized {
public void test() {
Long i = 1L;
synchronized (i) {
System.out.println();
}
}
javap -c コマンドを使用して上記のクラス ファイルを逆コンパイルすると、次のことがわかります。
上記からわかるように、コード ブロックにsynchronized が追加されると、ロック コード ロジックの前後に Monitorenter 命令と Monitorexit 命令が追加されます。
Monitorenter の一般的なプロセスは次のとおりです。
1. 監視エントリ数が 0 の場合、ロックの取得に成功し、エントリ数を 1 に設定して業務を実行します。
2. 現在のスレッドが同期ロック オブジェクトを取得するために再エントリした場合、それが同じスレッドであるかどうかを判断し、同じスレッドである場合は、監視エントリ数 (リエントラント ロック) に 1 を加算します。
3. この時点で他のスレッド A が現在の同期ロック オブジェクトを取得するために入った場合、モニター エントリの数が 0 ではなく、スレッド A によって保持されていないと判断され、スレッド A はブロックされ、スピンしてロックを待機し、モニター エントリの数が 0 に下がった後、スレッド A が再度ロックを取得します (ロック アップグレード)。
モニター終了プロセス:
現在モニターを保持しているスレッドは、monitorexit を呼び出し、命令が実行され、モニター エントリの数が 1 つ減ります。削減後にモニター エントリの数が 0 になった場合、現在のスレッドはロックを解放し、ロック リソースを待っている他のスレッドがロックの取得を試みることができます。
2.3.2 同期方法
同期コード ブロック メソッドのロック メソッドとは異なり、同期メソッドのロック メソッドは ACC_SYNCHRONIZED
識別子に基づいて実装されます。同期メソッドが呼び出されるとき、メソッドに ACC_SYNCHRONIZED 識別子が設定されているかどうかが最初にチェックされます。設定されている場合、スレッドは最初にモニタを取得します。取得が成功すると、メソッドを実行できるようになり、メソッドの実行後にモニタが解放されます。
インスタンス メソッドの場合、JVM はインスタンス オブジェクトのロックを取得しようとし、クラス メソッドの場合、JVM はクラス ロックの取得を試みます。正常復帰、異常復帰を問わず、同期メソッドが完了するとロックが解除されます。
ここでは詳しく説明しません。興味のある友人は自分で試してみてください。
2.4 ロックのアップグレード
前述したように、同期は組み込みのモニター ロック (モニター) によって実装されますが、モニター ロックは基本的に、基礎となるオペレーティング システムの Mutex ロック (相互排他ロック) によって実装されます。スレッド切り替えを実装するためにオペレーティング システムがユーザー状態からコア状態に切り替える必要があることを考慮すると、状態間の遷移には長い時間がかかり、このコストが非常に高くつくため、Synchronized は非効率的です。オペレーティング システムの Mutex Lock に依存するこの種のロックは、「重量ロック」と呼ばれます。
JDK1.6 より前には、ロック分類の概念がなく、同期は重量ロックでした。
しかし、JDK1.6 バージョン以降、CAS スピン、ロックの削除、およびロックの粗化が追加され、さらに、ロックなし、偏ったロック、軽量ロック、重量ロックのロック分類ステータスが導入されました。さまざまなロック競合条件に応じて、ロックのステータスは徐々にアップグレードされ、パフォーマンスは徐々に低下しますが、ほとんどの場合、ビジネスの同時実行性は高くなく、偏ったロックと軽量ロックは要件を満たすことができます。
JDK 1.6 では、バイアスされたロックと軽量ロックがデフォルトで有効になり、バイアスされたロックは -XX:-UseBiasedLocking によって無効にできます。
ロックはアップグレードできますが、ダウングレードはできません。
2.4.1 バイアスロック
中心となるアイデア: ロックされたコードが最初から最後まで 1 つのスレッドのみによって呼び出されると仮定します。複数のスレッド呼び出しがある場合、そのコードは軽量ロックにアップグレードされます。
つまり、バイアス ロックはコード ブロックのシングル スレッド実行にのみ適しています。マルチスレッドの場合、バイアス ロックは少なくとも軽量ロックにアップグレードする必要があるため、実際のビジネス条件に応じてバイアス ロックを無効にするかどうかを -XX:-UseBiasedLocking を使用して決定する必要があります。
バイアスロックのアップグレード:
1. スレッド A がロックを取得し、同期コード ブロックに入ることができます。仮想マシンは、現在のロック オブジェクト ヘッダーとスタック フレームにバイアスされたロックの threadId を記録します。つまり、バイアス モードに設定されます。同時に、CAS 操作を使用して、ロックを取得したスレッド ID をオブジェクトのマーク ワードに記録します。
2. スレッド A は再度ロックを取得し、現在のスレッドの threadId がロック オブジェクト ヘッダーの threadId と一致するかどうかを判断し、一致する場合は同じスレッドであることを意味し、追加およびロック解除に CAS を使用する必要はありません (CAS 操作によりローカル呼び出しが遅延します)。
3. このとき、スレッド B がロックの取得を開始します。現在のスレッドの threadId がロック オブジェクト ヘッダーの threadId と一致しない場合は、ロック オブジェクトに設定されている threadId に対応するスレッドが生きているかどうかを確認します。
- 生きている場合: スレッド A がロック オブジェクトを保持する必要があるかどうかを判断し、必要に応じてスレッド A を一時停止し、バイアスされたロックをキャンセルし、軽量ロックにアップグレードします。スレッド A がロック オブジェクトを保持する必要がなくなった場合は、ロック オブジェクトの状態をロック フリー状態に設定します。
- 存続するものが存在しない場合: ロック オブジェクトをロックのない状態にリセットすると、スレッド B が競合してそれをバイアスされたロックとして設定できます。
2.4.2 軽量ロック
中心的なアイデア: チェーンされたコードは同時実行されず、マルチスレッドの競合がない場合、ヘビーウェイトでの動作状態の遷移によって引き起こされるパフォーマンスの消費を削減します。
バイアスされたロックがオフになっている場合、またはバイアスされたロックに対するマルチスレッドの競合によりバイアスされたロックが軽量ロックにアップグレードされる場合、軽量ロックを取得しようとします。
1. スレッド A は同期コード ブロックを呼び出して、ロック オブジェクトの状態を確認します。オブジェクトがロックされていない場合 (ロック フラグが「01」、バイアスされたロックであるかどうかが「0」)、JVM はまず現在のスレッドのスタック フレームにロック レコード (ロック レコード) という名前のスペースを作成します。
2. オブジェクト ヘッダーのマーク ワードのコピー (つまり、置換されたマーク ワード) を手順 1 で作成したロック レコード領域にコピーします。その後、JVM は CAS を使用して、オブジェクトのマーク ワードをロック レコード (ロック レコード) へのポインターに更新しようとします。
3. 更新が成功した場合、スレッド A は現在のオブジェクト ロックを所有し、オブジェクト Mark Word のロック フラグを「00」(軽量ロック) に設定します。
4. 更新が失敗した場合、JVM はオブジェクトのマーク ワード内のロック ワードが現在のスレッドのスタック フレームを指しているかどうかをチェックします。ロックの競合がない場合 (例: スレッド B が最初にロックを取得する)、スレッド A はスピンし、スレッド B がロックを解放するのを待ちます。スレッド A のスピンが終了するか、スレッド A のスピン中にスレッド C もロックを競合すると、軽量ロックはヘビーウェイト ロックにアップグレードされます。ヘビーウェイト ロックは、CPU のアイドリングを防ぐために、ロックを所有するスレッド以外の他のスレッドをブロックします。
2.4.3 比較
ロック | アドバンテージ | 欠点がある | 該当シーン |
---|---|---|---|
バイアスロック | ロック解除とロック解除に追加の消費はなく、パフォーマンスは非同期メソッドを実行する場合とほぼ同じです。 | ロック競合が発生した場合、追加のロック解除消費が発生します | ほとんどの場合、単一スレッドがメソッド ブロックにアクセスするシナリオに適用できます。 |
軽量ロック | 競合するスレッドをブロックせず、プログラムの応答速度を向上させます。 | ロックの待機中にスピンし、CPU がアイドル状態になり、パフォーマンスが消費されます。 | スレッドロックの競合が少ないシナリオに適用でき、応答速度が向上します。 |
重量級ロック | スピンは適用されず、CPU がアイドル状態になることはありません | スレッドのブロック、長い応答時間 | マルチスレッドの競合するロックがスループットを追求するシナリオに適用可能 |
2.4.4 ロック粗大化
複数の連続したロックおよびロック解除操作を接続して、より広範囲のロックに拡張し、頻繁なロックおよびロック解除によるパフォーマンスの消費を回避します。
2.4.5 ロックの解除
ジャストインタイム コンパイルの段階で、JVM は実行コンテキストをスキャンし、エスケープ分析を実行して、共有リソースの競合が発生する可能性が低いロックを削除します。この方法で不要なロックを削除することで、冗長なリクエスト ロックによるパフォーマンスの消費を節約できます。例:
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
StringBuffer sb = new StringBuffer();
sb.append("concat1").append("concat2");
}
}
StringBuffer.append メソッドのソース コードを見てみましょう。
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
上の例のように、StringBuffer のスコープは main メソッド内にのみあり、main メソッドの外にはエスケープされません。JVM がコンパイル時にそのようなシナリオを検出すると、append メソッドを呼び出すために必要な同期が排除され、不必要なパフォーマンスの消費が削減されます。
ロック解除のサポートを有効にする
ロックを削除するには、プログラムをサーバー モードで実行し (サーバー モードはクライアント モードよりも最適化されます)、-XX:+DoEscapeAnalysis を通じてエスケープ分析を有効にし、-XX:+EliminateLocks を通じてロックの削除を有効にする必要があります。
例: -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks