Javaマルチスレッドのsynchronizedとその最適化
7248 ワード
Synchronizedと同期ブロック
synchronized
はjvmによって提供される同期およびロック機構であり、これに対応してjdkレベルのJ.U.Cによって提供されるAbstractQueuedSynchronizer
に基づく同時コンポーネントである.synchronized
は反発同期を提供し、反発同期とは、複数のスレッドが共有データに同時にアクセスする場合に、共有データが同じ時刻に1つのスレッドしかアクセスしないことを保証することを意味する.jvmでは、synchronized
で修飾されたコードブロックがjavacでコンパイルされると、ロックおよびロック解除するオブジェクトを指定するために、コードブロックの前後にmonitorenterおよびmoniterexitバイトコード命令がそれぞれ生成されます.synchronized
でオブジェクトパラメータが指定されている場合、referenceはそのオブジェクトの参照であり、手動で指定されていない場合は、synchronized
に従ってインスタンスメソッドかクラスメソッドかを修飾し、対応するオブジェクトインスタンスまたはClassオブジェクトをロックオブジェクトとして取得します.特筆すべきは、javaにおけるObject
クラスには、wait()
およびnotify()
(notifyAll()
はnotify()
と類似)の2つの方法と同期ロックが関連している.この2つの方法がなぜObject
クラスに配置されるのかについては、wait()
メソッドの意味は、現在のスレッド(wait()
メソッドを呼び出すスレッド)を待つことであり、notify()
メソッドによって呼び出されることを知っているからである.現在のスレッドにはオブジェクトのロックが必要です.wait()
メソッドを呼び出すと、現在のスレッドは同期オブジェクトのロックを解放します.したがって、wait()
メソッドは、synchronized
によって修飾されたコードブロック内(同期ロックが必ず取得される)でなければならない.いずれのJavaオブジェクトもオブジェクトロックとして機能するため、この2つのメソッドはObject
に配置する必要がある.Javaスレッドはオペレーティングシステムのオリジナルスレッドにマッピングされており、スレッドをブロックまたは起動するには、カーネルにアクセスして完了する必要があります.このようなユーザー状態からカーネル状態への状態変換には多くのプロセッサ時間がかかるため、synchronized
はJavaで非常にリソースを消費する操作です.JDK 1.5以前、synchronized
はJDKベースのReentrantLock
に比べて性能がかなり低かった.JDK 1.6以降のバージョンでは、synchronized
はロックに対する多くの最適化措置を実現し、これらの最適化の大部分はReentrantLock
の実現構想と似ている.非ブロック同期とCAS
ブロック同期は、常にロックの追加とロックの解除に関連します.これは、ユーザー状態とカーネル状態の切り替えを意味し、リソースを非常に消費します.このため、衝突検出に基づく楽観的な同時戦略が誕生した.この戦略の核心思想は、まずデータを操作し、他のスレッドがデータを競合しなければ、操作が成功したと考えている.そうでなければ、データ操作の成功を保証するために他の方法を採用します(たとえば、再試行し続け、成功を知っています).この同時ポリシーは、スレッドを一時停止する必要がないので、非ブロック同期と呼ばれます.CASはCompareAndSwapの略称で、メモリ位置、予想値、更新値の3つのオペランドが必要です.CAS命令が実行されると、プロセッサはまずメモリ位置の値が予想値と等しいかどうかを判断し、等しい場合、予想値を更新値に更新する.そうでなければ、他のスレッドがこのメモリ位置の値を変更したと考えられ、更新操作は許可されません.更新操作が発生するかどうかにかかわらず、CAS操作は予想値を返し、CAS操作は原子操作です.CASは非ブロック同期に非常に適しており、例えばint型の値iに1を加えるには、変数iのメモリ位置の値が変更されているかどうかを判断し、そうでなければ、この位置の値をi+1に更新することを考えている.そうでなければ、他のスレッドがこの値を変更したと考えられ、更新が成功するまで、前の操作を再呼び出して更新を試みることができます.なぜなら,メモリ位置の値を判断する際にはまずこのメモリ位置の値を取得する必要があり,Javaでは,この変数の可視性を保証するために
volatile
キーワードで修飾する必要があるからである.JDK 1.5後、Javaプログラムはsun.misc.Unsafe
クラスの基本タイプのCAS操作を実現したパッケージを提供し、JUCコンポーネントはこのクラスに基づいて実現された.java.util.concurrent.atomic.AtomicInteger
クラスを例にとると、このクラスにはインスタンス変数valueがパッケージされています.private volatile int value;
この変数の自己増加を実現するソースコードは、次のとおりです.
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
そのうち、
Unsafe#getAndAddInt()
の実装は以下の通りである.public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
ここでvar 2はメモリアドレスの値であり、var 5は予想値であり、compareAndSwapInt()メソッドがtrueを返すと、このCAS操作が成功したことを示し、そうでなければ再試行し、成功するまで知る.ここから、CAS操作の3つの欠点を発見することができる.
synchronized
のようにコードの同期を実現できない.ここで、unsafeはUnsafe
の例で、このクラスはユーザープログラムに提供されるクラスではありません.その中にパッケージされているのはほとんどJNIメソッドです.プログラマーは一般的にこのクラスを使用する必要はありません.JavaのCAS操作のパッケージであることを知っていればいいだけです.ロックの最適化
前述の
synchronized
に対応する同期ロックは比較的重量級であるため、JDK 1.5以降、開発者は、適応スピンロック、ロック消去、ロック粗化、軽量ロック、偏向ロックなど、様々なロック最適化技術を実現した.なお、これらのロックはいずれも仮想マシンレベルの最適化であり、synchronized
に対応するバイトコードの最適化と考えられる.スピンロックとアダプティブスピンロック
スピンロックは仮想マシン開発者の共有データの統計分析から得られた最適化技術であり、多くの場合、共有データのロック状態は短い時間しか続かないが、他のスレッドはロックを待っている間に、この待機時間のためにスレッドを停止し、回復する価値はない.スレッドを保留せずに少し待つために、スレッドに忙しいサイクル(スピン)を実行させることができます.これがスピンロックです.なお、スピンロックはCPUリソースを消費する必要があり、たまたま共有データのロック時間が長いと、逆にスピンロックの性能が低下する.適応スピンロックはスピンロックの最適化であり,盲目的な待機を避けるためにスピン時間を動的に調整することができる.
ロック解除
ロック除去はjavacレベルでの最適化です.一部のプログラマが作成した
synchronized
で修飾されたコードを呼び出すか呼び出すが、共有データ競合が存在しないことが検出された場合、javacはこのコードを最適化し、余分な同期命令を除去する.ロック粗化
原則として、プログラムを作成する際には、同期ブロックが小さいほど良いことが要求されるが、一連の連続操作が同じオブジェクトに対する反復ロックとロック解除である場合、リソースの浪費であり、この場合、ロックの範囲を操作シーケンス全体に拡大することは、反復ロックとロック解除の占有リソースを節約するのに有利である.ロック除去およびロック粗化技術はjavacまたはjvmのインテリジェントな最適化である.
軽量ロック
軽量ロックは、マルチスレッド競合がないことを前提として、従来の重量ロックがオペレーティングシステムの反発量を使用して発生するパフォーマンス消費を低減します.軽量ロックの実現根拠は、「ほとんどの場合、同期サイクル全体で競合は存在しない」ことであり、競合が存在しない場合、軽量ロックを使用するにはオペレーティングシステムの反発量を使用する必要がなく、リソースを節約できます.しかしながら、競合が存在する場合、軽量レベルロックにはCASオーバーヘッドと従来の重量レベルロック動作のオーバーヘッドが存在し、性能がより低くなる.軽量ロックの実装には、ロックとロック解除の2つのプロセスがあります.
バイアスロック
バイアスロックは軽量レベルのロックに比べてより急進的な方法である:競合なしに同期全体を除去し、バイアスロックはロックが最初に取得したスレッドに偏っていることを意味し、次の実行中にロックが他のスレッドに取得されなければ、バイアスロックを持つスレッドは同期を必要としない.仮想マシンは-XX:+UseBiasedLockingパラメータを使用してヨーロックを開始し、ロック対象がスレッドによって初めて取得されると、仮想マシンはオブジェクトヘッダのフラグビットを「01」--ヨーロックモードに設定します.同時にCAS操作を用いてこのロックを取得したスレッドのIDをオブジェクトのMark Wordに記録し、CAS操作が成功すれば、偏向ロックを持つスレッドがこのロックに関連する同期ブロックに入るたびに、仮想マシンは同期操作を行わなくてもよい.別のスレッドがこのロックを取得しようとすると、バイアスロックは終了します.このとき,ロック対象が現在ロック状態にあるか否かにより,偏向を解消して未ロックまたは軽量ロックの状態に復帰し,後続の同期動作は前述した軽量ロックのように実行される.
参考:「Java仮想マシンを深く理解する」第2版、周志明