マルチスレッドにはいろいろなことがありますし、とても面白いので、最近はマルチスレッドの方向に焦点を当てていたのかもしれませんが、気に入っていただけませんか?
1.楽観的ロックと悲観的ロック
1.1シナリオ
通常、次のシナリオではSynchronizedを使用します。
- 変更されたインスタンスメソッド、現在のインスタンスオブジェクトをロックします
public class Synchronized {
public synchronized void husband(){
}
}
- 静的メソッドを変更し、現在のクラスのClassオブジェクトをロックします
public class Synchronized {
public void husband(){
synchronized(Synchronized.class){
}
}
}
- コードブロックを変更し、ロックされたオブジェクトを指定して、オブジェクトをロックします
public class Synchronized {
public void husband(){
synchronized(new test()){
}
}
}
実際、これはロックメソッド、ロックコードブロック、およびロックオブジェクトですが、どのようにロックを実装しますか?
その前に、Javaオブジェクトの構成について説明します
。JVMでは、オブジェクトはメモリ内で3つの領域に分割されます。
-
オブジェクトヘッダー
- マークワード(マークされたフィールド):オブジェクトのHashCode、世代経過時間、およびロックフラグ情報がデフォルトで保存されます。オブジェクトの状態に応じて独自のストレージスペースを再利用します。つまり、操作中にロックフラグが変更されると、マークワードに保存されているデータが変更されます。
- Klass Point(型ポインター):クラスメタデータへのオブジェクトのポインター。仮想マシンはこのポインターを使用して、このオブジェクトがどのクラスインスタンスであるかを判別します。
-
インスタンスデータ
- この部分は、主にクラスのデータ情報と親クラスの情報を格納します。
-
それを埋める
- 仮想マシンでは、オブジェクトの開始アドレスが8バイトの整数倍である必要があるため、バイトの位置合わせのためだけに、パディングデータが存在する必要はありません。
ヒント:空のオブジェクトが何バイトを占めるかを尋ねられたことはありますか?アラインメントとパディングのため、8バイトです。8バイト未満が自動的に入力されます。
- 仮想マシンでは、オブジェクトの開始アドレスが8バイトの整数倍である必要があるため、バイトの位置合わせのためだけに、パディングデータが存在する必要はありません。
順序、可視性、原子性についてよく話しますが、同期はどのように行われますか?
1.2秩序
揮発性の章で、コードを最適化するためにCPUがプログラムを並べ替えるとすでに述べました。
as-if-serial
コンパイラとCPUがどのように並べ替えるかに関係なく、プログラムの結果がシングルスレッドの場合に正しいこと、およびデータに依存している場合でも並べ替えられないことを確認する必要があります。
例えば:
int a = 1;
int b = a;
2つの段落を並べ替えることはできません。bの値はaの値によって異なります。aが最初に割り当てられていない場合は、空になります。
1.3可視性
また、「揮発性」の章では、最近のコンピュータのメモリ構造とJMM(Javaメモリモデル)を紹介しました。ここでは、JMMは実際には存在しないが、一連の仕様について説明する必要があります。この仕様では、多くのJavaプログラムについて説明しています。変数(スレッド共有変数)アクセスルール、およびメモリへの変数の格納とJVMのメモリからの変数の読み取りの低レベルの詳細、Javaメモリモデルは、共有データルールと保証の可視性、順序、およびアトミック性です。
誰もが興味を持っており、コンピュータ、CPU、メモリ、マルチレベルキャッシュなどのコンポーネントを理解することを忘れないでください。これは、Javaがこれを行う理由をよりよく理解するのに役立ちます。
1.3アトミシティ
実際、原子性を確保するのは非常に簡単です。1つのスレッドだけが同時にロックを取得し、コードブロックに入ることができるようにするだけで十分です。
これらは、ロックを使用するときによく使用する機能です。同期自体にはどのような機能がありますか?
1.4再入可能
同期時にオブジェクトをロックするとカウンターがあり、スレッドがロックを取得した回数を記録します。対応するコードブロックが実行されると、カウンターは-1になります。カウンターがクリアされるまで、ロックは解除されます。
再入国のメリットは何ですか?
これにより、デッドロック状態を回避でき、コードをより適切にカプセル化することもできます。
1.5中断のない
無停電とは、スレッドがロックを取得した後、別のスレッドがブロックまたは待機状態にあることを意味します。前者のスレッドは解放されず、後者は常にブロックまたは待機し、中断することはできません。
LockのtryLockメソッドは中断される可能性があることに注意してください。
2.低レベルの実装
ここでの実装は非常に単純です。ロックメソッドとロックコードブロックを使用して単純なクラスを作成しました。バイトコードファイルを逆コンパイルしてみましょう。問題はありません。
私が書いたテストクラスを最初に見てください:
/**
*@Description TODO Synchronize
*@Author: ZhangSan_Plus
*@Date: 2020/6/15 15:03
**/
public class Synchronized {
public synchronized void husband(){
synchronized(new Volatile()){
}
}
}
コンパイルが完了したら、対応するディレクトリに移動し、javap -c xxx.classコマンドを実行して、逆コンパイルされたファイルを表示します。
Classfile /Users/aobing/IdeaProjects/Thanos/laogong/target/classes/juc/Synchronized.class
Last modified 2020-5-17; size 375 bytes
MD5 checksum 4f5451a229e80c0a6045b29987383d1a
Compiled from "Synchronized.java"
public class juc.Synchronized
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#14 // java/lang/Object."<init>":()V
#2 = Class #15 // juc/Synchronized
#3 = Class #16 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Ljuc/Synchronized;
#11 = Utf8 husband
#12 = Utf8 SourceFile
#13 = Utf8 Synchronized.java
#14 = NameAndType #4:#5 // "<init>":()V
#15 = Utf8 juc/Synchronized
#16 = Utf8 java/lang/Object
{
public juc.Synchronized();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljuc/Synchronized;
public synchronized void husband();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 这里
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class juc/Synchronized
2: dup
3: astore_1
4: monitorenter // 这里
5: aload_1
6: monitorexit // 这里
7: goto 15
10: astore_2
11: aload_1
12: monitorexit // 这里
13: aload_2
14: athrow
15: return
Exception table:
from to target type
5 7 10 any
10 13 10 any
LineNumberTable:
line 10: 0
line 12: 5
line 13: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Ljuc/Synchronized;
}
SourceFile: "Synchronized.java"
2.1同期コード
私がマークした場所がいくつかあります。最初にオブジェクトヘッダーについて説明しました。これは、モニターオブジェクトに関連付けられます。
- 私がマークした場所がいくつかあります。最初にオブジェクトヘッダーについて説明しました。これは、モニターオブジェクトに関連付けられます。
- すでにこのモニターの所有者であり、再度入力すると、入力番号は+1になります。
- 同様に、monitorexitの実行が終了すると、対応するエントリ番号は-1になり、0になるまで、他のスレッドで保持できます。
実際、すべての相互排除は、モニターの所有権を取得できるかどうかを確認するためのものです。所有者になると、あなたが勝者になります。
2.2同期方法
メソッドに特別なフラグACC_SYNCHRONIZEDがあることに気付いたかどうかはわかりません。
メソッドを同期するとき、メソッドが実行されると、最初にフラグビットがあるかどうかが判別され、次にACC_SYNCHRONIZEDが2つの命令(monitorenterとmonitorexit)を暗黙的に呼び出します。
したがって、最終的な分析では、それはまだモニターオブジェクトの競争です。
2.3モニター
私はこのオブジェクトについて何度も話しましたが、それは虚無主義的なものだと思いますか、実際はそうではありません。モニターモニターのソースコードは、仮想マシンのObjectMonitor.hppファイルにC ++で記述されています。
ソースコードを見ると、彼のデータ構造は次のようになっています。
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 线程重入次数
_object = NULL; // 存储Monitor对象
_owner = NULL; // 持有当前线程的owner
_WaitSet = NULL; // wait状态的线程列表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 单向列表
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁状态block状态的线程列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
また、このC ++コードをオープンソースプロジェクトに入れているので、自分で確認できます。
同期の下部にあるソースコードはObjectMonitorの紹介です。興味があれば、それをチェックしてください。とにかく、私が上で言ったことと、よく耳にする概念は、ここにあります。
おなじみのロックアップグレードプロセスは実際にはソースコードにあり、さまざまな実装を呼び出してロックを取得し、失敗した場合は上位レベルの実装が呼び出されてアップグレードが完了すると誰もが言っていました。
3.ヘビーウェイトロック
ObjectMonitorのソースコードを見ると、Atomic :: cmpxchg_ptr、Atomic :: inc_ptr、その他のカーネル関数があります。対応するスレッドは、park()とupark()です。
この操作には、ユーザーモードとカーネルモードの間の変換が含まれます。このスイッチは非常にリソースを消費するため、スピンロックのような操作がある理由を理解してください。エンドレスループのような操作は、より多くのリソースを消費すると言っても過言ではありません。実際にはそうではありません。見つければわかるでしょう。
3.1ユーザーモードとカーネルモードとは何ですか?
Linuxシステムのアーキテクチャは、ユーザースペース(アプリケーションプログラムアクティビティスペース)とカーネルに分割されている大学と接触している必要があります。
すべてのプログラムはユーザースペースで実行されており、ユーザー実行状態に入るのは(ユーザーモード)ですが、多くの操作にはカーネル実行が含まれる場合があります。I/ Oを使用すると、カーネル実行状態(カーネルモード)になります。
このプロセスは非常に複雑で、多くの値の転送が含まれます。プロセスを簡単に要約します。
- ユーザーモードは、一部のデータをレジスタに配置するか、対応するスタックを作成して、オペレーティングシステムによって提供されるサービスが必要であることを示します。
- ユーザーモードはシステムコールを実行します(システムコールはオペレーティングシステムの最小の機能単位です)。
- CPUはカーネルモードに切り替わり、対応するメモリ位置にジャンプして命令を実行します。
- システムはプロセッサを呼び出して、以前にメモリに格納したデータパラメータを読み取り、プログラムの要求を実行します。
- 呼び出しが完了すると、オペレーティングシステムはCPUをユーザーモードにリセットして結果を返し、次の命令を実行します。
そのため、1.6より前は重量級のロックだったと誰もがいつも言っていましたが、その重量の本質はObjectMonitorの呼び出しプロセスとLinuxカーネルの複雑な操作メカニズムによって決まります。多くのシステムリソースを消費するため、効率が悪い。
カーネルモードとユーザーモードが切り替わる状況も2つあります。異常なイベントと周辺機器の割り込みも理解できます。
4ロックのアップグレードを最適化する
効率が悪いと言われており、関係者も知っているのでアップグレードしました。先ほどのソースコードを読めば、アップグレードは実はとてもシンプルで、あと数回の関数呼び出しで簡単にできることがわかります。 。、しかし、デザインはまだ非常に巧妙です。
アップグレード後のロックアップグレードプロセスを見てみましょう:
シンプルバージョン:
アップグレードの方向:
ヒント:このアップグレードプロセスは元に戻せないことを忘れないでください。最後に、使用シナリオを含めて、その影響について説明します。
彼のアップグレードを見た後、各ステップの実行方法について話しましょう。
4.1バイアスロック
前述したように、オブジェクトヘッダーはMark WordとKlassポインタで構成され、ロック競合はオブジェクトヘッドが指すMonitorオブジェクトの競合です。スレッドがオブジェクトを保持すると、フラグビットが1に変更され、バイアスモードになります。そして、このスレッドのIDがオブジェクトのマークワードに記録されます。
このプロセスはCAS楽観的ロック操作を使用します。同じスレッドが入るたびに、仮想マシンは同期操作を実行しません。フラグビットを+1に追加するだけです。異なるスレッドが来ると、CASは失敗し、ロックが失敗したことを意味します。 。
バイアスロックは、1.6以降はデフォルトでオンになり、1.5ではオフになります。手動でオンにする必要があるパラメーターはxxです。-UseBiasedLocking= false。
バイアスロックが閉じている場合、または複数のスレッドがバイアスロックをめぐって競合する場合はどうなりますか?
4.2軽量ロック
それはまだマークワークに関連しています。オブジェクトがロックフリーの場合、jvmは現在のスレッドのスタックフレームにロックレコードと呼ばれるスペースを作成して、ロックオブジェクトのマークワードコピーを格納し、ロックレコードを設定します。の所有者は、現在のオブジェクトを指します。
次に、JVMはCASを使用して、オブジェクトの元のマークワードをロックレコードのポインタに更新しようとします。成功した場合は、ロックが成功したことを意味し、ロックフラグビットを変更して、関連する同期操作を実行します。
失敗した場合は、現在のオブジェクトのマークワードが現在のスレッドのスタックフレームを指しているかどうかを判断します。そうである場合は、現在のスレッドがすでにこのオブジェクトのロックを保持していることを意味します。そうでない場合は、他のスレッドによって保持されていることを意味します。スレッド。ロックのアップグレードと変更を続行します。ロックの状態、および待機中のスレッドもブロックされます。
4.3スピンロック
Linuxシステムのユーザーモードとカーネルモードの切り替えはリソースを消費することは前述しませんでしたが、実際にはスレッドの待機プロセスです。では、どうすればこの消費を減らすことができるでしょうか。
スピン、今やってきたものは、スレッドが中断されるのを防ぐためにスピンを続けます。リソースが取得されると、しきい値を超えるまで直接成功しようとします。スピンロックのデフォルトサイズは10倍です。-XX :PreBlockSpinは変更できます。
スピンが失敗した場合は、1.5のようなヘビーウェイトロックにアップグレードして、目覚めるのを待ちます。
これまでのところ、私は基本的に同期の概念の前後について話しましたが、誰もがそれを消化します。
参照:「高並行性プログラミング」、「ダークホースプログラマーズハンドアウト」、「JVM仮想マシンの詳細な理解」
5.同期またはロックを使用する必要がありますか?
それらの違いを見てみましょう:
- Synchronizedはキーワードであり、JVMレベルの最下層であり、Lockはインターフェースであり、JDKレベルの豊富なAPIです。
- Synchronizedは自動的にロックを解除し、Lockは手動でロックを解除する必要があります。
- 同期は中断できません。ロックは中断できるかどうかはわかりません。
- Lockを使用すると、スレッドがロックを取得したかどうかを知ることができますが、同期はできません。
- Synchronizedはメソッドとコードブロックをロックできますが、Lockはコードブロックのみをロックできます。
- Lockは、読み取りロックを使用して、マルチスレッドの読み取り効率を向上させることができます。
- Synchronizedは非フェアロックです。ReentrantLockはそれがフェアロックであるかどうかを制御できます。
2つのうちの1つはJDKレベルで、もう1つはJVMレベルです。最大の違いは、実際にはリッチAPIが必要かどうかであり、別のシナリオがあります。
たとえば、私はディディです。午前中にラッシュアワーがあり、コードで多くの同期が使用されています。問題は何ですか。ロックのアップグレードプロセスは元に戻せません。ピーク後も、まだ重量級のロックです。効率は大幅に低下していますか?この時点でロックを使用するのは良いですか?
シナリオを検討する必要があります。ビジネスがなければ、すべての技術的な議論は価値がないため、現在どちらが良いかはナンセンスです。