まとめ
このシリーズは、数字をゴールドに精製する過程に基づいており、より良く学ぶために一連のレコードが作成されました。この記事では、主に次のように紹介されています。1。ロック最適化のアイデアと方法2。仮想マシンのロック最適化3。Locksの誤った使用の場合4。Threadlocalとそのソースコード分析
1。ロック最適化のアイデアと方法
並行性のレベルは、[高い並行性Java 1]の紹介で言及されています。
ロックを使用すると、これがブロックされていることを意味するため、一般的に並行性はロックフリーの状況よりも少し低くなります。
ここで言及したロックの最適化は、ブロッキングの場合にパフォーマンスが貧弱になるのを防ぐ方法を指します。しかし、どのように最適化しても、パフォーマンスは一般的にロックフリーの状況よりも少し悪いでしょう。
ここでは、[scurrency java v] jdk concurrencyパッケージ1に記載されているReentrantlockのtryLockは、トリロックの裁判官が吊るされないため、ロックフリーの方法である傾向があることに注意する必要があります。
ロック最適化のアイデアと方法を要約するには、次のタイプがあります。
1.1ロック保持時間を短縮します
public synchronized void syncmethod(){othercode1(); mutextMethod(); otherCode2(); }上記のコードと同様に、メソッドを入力する前にロックを取得する必要があり、他のスレッドは外で待つ必要があります。
ここでの最適化ポイントは、他のスレッドの待ち時間を短縮することです。そのため、スレッドの安全要件を備えたプログラムにロックを追加するためにのみ使用されます。
public void syncmethod(){othercode1();同期(this){mutextmethod(); } otherCode2(); }1.2ロック粒子サイズを縮小します
大きなオブジェクト(このオブジェクトには多くのスレッドでアクセスされる場合があります)を小さなオブジェクトに分割し、並列性を大幅に増加させ、ロック競合を低減します。ロックの競争を減らし、ロックに向かってバイアスをかけることによってのみ、軽量ロックの成功率が改善されます。
ロックの粒度を減らす最も典型的なケースは、同時ハッシュマップです。これは、[high scurrency java v] jdk concurrencyパッケージ1で言及されています。
1.3ロック分離
最も一般的なロック分離は、読み取りワイトロックreadwritelockです。これは、関数に応じて読み取りワイトロックと書き込みロックに分離されています。このようにして、読み取りと読書は相互に排他的ではなく、読書と執筆は相互に排他的であり、スレッドの安全性を保証し、パフォーマンスを向上させます。詳細については、[scurrency java v] jdk concurrencyパッケージ1を確認してください。
読み書きの分離のアイデアを拡張することができ、操作が互いに影響を与えない限り、ロックを分離することができます。
たとえば、LinkedBlockingQueue
頭からそれを取り出して、尾からデータを置きます。もちろん、それは[High Concurrency Java VI] JDK Concurrencyパッケージ2で言及されているForkjoinpoolの作業盗難にも似ています。
1.4ロックラーニング
一般的に言えば、複数のスレッド間の効果的な並行性を確保するために、各スレッドはロックをできるだけ短く保持する必要があります。つまり、ロックは公開リソースを使用した直後にリリースする必要があります。この方法でのみ、このロックを待っている他のスレッドでは、できるだけ早くタスクを実行するリソースを取得できます。ただし、すべてに学位があります。同じロックが常に要求され、同期し、リリースされている場合、システムの貴重なリソースを消費しますが、これはパフォーマンスの最適化を助長しません。
例えば:
public void demomethod(){synchronized(lock){// do sth。 } //他の不要な同期作業を実行しますが、迅速に同期することができます(lock){// do sth。 }}この場合、ロックラービングのアイデアによれば、マージする必要があります
public void demomethod(){//ロックリクエストに統合する(lock){// do sth。 //他の不要な同期作業を実行しますが、迅速に実行できます}}もちろん、前提条件があり、同期を必要としない中央の作業は迅速に実行されます。
別の極端な例を挙げましょう:
for(int i = 0; i <circle; i ++){synchronized(lock){}}ロックはループで取得する必要があります。 JDKはこのコードを内部的に最適化しますが、直接記述する方が良いです
同期(lock){for(int i = 0; i <circle; i ++){}}もちろん、そのようなループが長すぎて、あまり長く待たないように他のスレッドを与える必要があると言う必要がある場合は、上記のようにしか書くことができません。そのような同様の要件がない場合は、次の要件に直接書き込むことをお勧めします。
1.5ロックエリミネーション
ロックエリミネーションはコンパイラレベルのものです。
インスタントコンパイラでは、共有できないオブジェクトが見つかった場合、これらのオブジェクトのロック操作を排除できます。
いくつかのオブジェクトに複数のスレッドでアクセスできないため、なぜロックを追加する必要があるのか、奇妙なことになるでしょう。コードを書くときにロックを追加しない方が良いのではないでしょうか。
しかし、時には、これらのロックはプログラマーによって書かれていません。それらのいくつかは、VectorやStringBufferなどのクラスなど、JDK実装にロックされています。それらの方法の多くにはロックがあります。スレッドの安全性なしにこれらのクラスの方法を使用すると、特定の条件が満たされると、コンパイラはロックを削除してパフォーマンスを向上させます。
例えば:
public static void main(string args [])throws arturtedexception {long start = system.currenttimemillis(); for(int i = 0; i <2000000; i ++){createStringBuffer( "jvm"、 "診断"); } long buffercost = system.currenttimemillis() - start; System.out.println( "craetestringbuffer:" + buffercost + "ms"); } public static string createStringbuffer(string s1、string s2){stringbuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.tostring(); } StringBuffer.Append上記のコードは同期操作ですが、StringBufferはローカル変数であり、メソッドはStringBufferを返さないため、複数のスレッドがアクセスすることは不可能です。
StringBufferの同期操作は、現時点では無意味です。
ロックキャンセルはJVMパラメーターに設定されています。もちろん、サーバーモードである必要があります。
-server -xx:+doscapeanalysis -xx:+eliminatelocks
エスケープ分析をオンにします。エスケープ分析の機能は、変数がスコープから逃げる可能性があるかどうかを確認することです。
たとえば、上記のStringBufferでは、上記のコードのCraetestringBufferの戻りは文字列であるため、このローカル変数StringBufferは他の場所では使用されません。 craetestringbufferを変更した場合
public static stringbuffer craetestringbuffer(string s1、string s2){stringbuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); SBを返します。 }次に、このstringbufferが返された後、他の場所で使用される場合があります(たとえば、メイン関数は結果を返してマップなどに入れます)。その後、JVMエスケープ分析は、このローカル変数StringBufferがその範囲を逃れることを分析できます。
したがって、エスケープ分析に基づいて、JVMは、ローカル変数StringBufferがそのスコープを逃がさない場合、StringBufferに複数のスレッドでアクセスしないことを判断でき、これらの追加ロックを削除してパフォーマンスを改善できると判断できます。
JVMパラメーターが次の場合:
-server -xx:+doscapeanalysis -xx:+eliminatelocks
出力:
craetestringbuffer:302 ms
JVMパラメーターは次のとおりです。
-server -xx:+doscapeanalysis -xx:-eliminatelocks
出力:
craetestringbuffer:660 ms
明らかに、ロック除去効果はまだ非常に明白です。
2。仮想マシンのロック最適化
まず、オブジェクトヘッダーを導入する必要があります。 JVMには、各オブジェクトにはオブジェクトヘッダーがあります。
マークワード、オブジェクトヘッダーのマーカー、32ビット(32ビットシステム)。
ハッシュ、ロック情報、ガベージコレクションタグ、年齢について説明してください
また、ロックレコードへのポインター、モニターへのポインター、バイアスされたロックスレッドIDなども保存されます。
簡単に言えば、オブジェクトヘッダーは、体系的な情報を保存することです。
2.1ポジティブロック
いわゆるバイアスは偏心です。つまり、ロックは現在ロックを所有しているスレッドに向かっている傾向があります。
ほとんどの場合、競争はありません(ほとんどの場合、同期ブロックには同時に競合ロックで複数のスレッドがありません)ため、バイアスをかけることでパフォーマンスを改善できます。つまり、競合がない場合、ロックを以前に取得したスレッドがロックを再度取得すると、ロックが私に向かっているかどうかが判断されるため、スレッドは再びロックを取得する必要がなく、同期ブロックに直接入ることができます。
バイアスロックの実装は、オブジェクトヘッダーマークのマークをバイアスとして設定し、スレッドIDをオブジェクトヘッダーマークに書き込むことです。
他のスレッドが同じロックを要求すると、バイアスモードが終了します
JVMは、デフォルトでバイアスロックを有効にします-XX:+useBiasedLocking
激しい競争では、偏ったロックがシステムの負担を増加させます(それが偏っているかどうかの判断は毎回追加されます)
バイアスロックの例:
パッケージテスト; import java.util.list; import java.util.vector; public class test {public static list <integer> numberlist = new Vector <Integer>(); public static void main(string [] args)throws arturtedexception {long begin = system.currenttimemillis(); int count = 0; int startnum = 0; while(count <10000000){numberlist.add(startnum); startnum += 2; count ++; } long end = system.currenttimemillis(); system.out.println(end -begin); }} Vectorは、ロックメカニズムを内部的に使用するスレッドセーフクラスです。追加するたびに、ロックリクエストが行われます。上記のコードには1つのスレッドメインのみがあり、その後、ロックリクエストを繰り返し追加します。
次のJVMパラメーターを使用して、バイアスロックを設定します。
-xx:+useBiasedLocking -xx:biasedLockingStartUpDelay = 0
BiasedLockingStartUpdelayは、システムが数秒間開始された後にバイアスロックが有効になることを意味します。システムが起動すると、一般的なデータ競争が比較的激しいため、デフォルトは4秒です。この時点で有効なバイアスロックはパフォーマンスを低下させます。
ここでは、バイアスロックのパフォーマンスをテストするために、遅延バイアスロック時間が0に設定されています。
この時点で、出力は9209です
下のバイアスロックをオフにします。
-xx:-USEBIASEDLOCKING
出力は9627です
一般に、競争がない場合、バイアスロックを有効にすることのパフォーマンスは約5%改善されます。
2.2軽量ロック
Javaのマルチスレッド安全性は、ロックメカニズムに基づいて実装されており、ロックのパフォーマンスは満足のいくものではありません。
その理由は、MultiThread同期を制御する2つのバイトコードプリミティブであるMonitorenterとMonitorexitがJVMによって実装されているため、オペレーティングシステムのMutexに依存しています。
Mutexは比較的リソースを提供する操作であり、スレッドをハングアップさせ、短期間で元のスレッドに再スケジュールする必要があります。
Javaのロックメカニズムを最適化するために、Java6以来、軽量ロックの概念が導入されています。
軽量ロックは、Mutexを置き換えるのではなく、マルチスレッドがミューテックスに入る可能性を減らすことを目的としています。
CPU Primitive Compare-and-Swap(CAS、Assembly Instruction CMPXCHG)を使用し、Mutexに入る前に治療を試みます。
バイアスロックが失敗した場合、システムは軽量ロック操作を実行します。その存在の目的は、そのパフォーマンスが比較的低くなるため、オペレーティングシステムレベルでミューテックスを可能な限り使用することを避けることです。 JVM自体はアプリケーションであるため、アプリケーションレベルでスレッド同期問題を解決したいと考えています。
要約すると、軽量ロックは高速ロック方法です。 Mutexを入力する前に、CAS操作を使用してロックを追加します。パフォーマンスを改善するために、オペレーティングシステムレベルでMutexを使用しないようにしてください。
次に、バイアスロックが失敗すると、軽量ロックのステップ:
1.オブジェクトヘッダーのマークポインターをロックされたオブジェクトに保存します(ここのオブジェクトは、同期(this){}などのロックされたオブジェクトを指します。これはここでオブジェクトです)。
lock-> set_displaced_header(mark);
2.オブジェクトヘッダーをロックへのポインターとして設定します(スレッドスタックスペース)。
if(mark ==(markoop)atomic :: cmpxchg_ptr(lock、obj() - > mark_addr()、mark)){tevent(slow_enter:release stacklock);戻る ; }ロックはスレッドスタックにあります。したがって、スレッドがこのロックを保持しているかどうかを判断するには、オブジェクトヘッダーが指すスペースがスレッドスタックのアドレス空間にあるかどうかを判断するだけです。
軽量ロックが失敗した場合、オペレーティングシステムレベルでの同期方法であるヘビー級ロック(通常のロック)への競争とアップグレードがあります。ロック競争がない場合、軽量ロックは、OSミューテックスを使用した従来のロックによって引き起こされるパフォーマンスの損失を減らします。競争が非常に激しい場合(軽量ロックは常に故障します)、軽量ロックは多くの余分な操作を行い、パフォーマンスの劣化をもたらします。
2.3スピンロック
軽量ロックの試行が失敗するため、競争が存在する場合、オペレーティングシステムレベルの相互除外を使用するために、ヘビー級ロックに直接アップグレードされる可能性があります。また、スピンロックを再試行することも可能です。
スレッドが迅速にロックを取得できる場合、OSレイヤーにスレッドを掛けることはできません。スレッドにいくつかの空の操作(スピン)を実行し、常にロックを取得しようとします(TryLockに似ています)。もちろん、ループの数は限られています。ループの数に達すると、ヘビー級ロックにアップグレードされます。したがって、各スレッドにロックを保持する時間がほとんどない場合、スピンロックはOSレイヤーでスレッドが吊り下げられないようにします。
jdk1.6 -xx:+usespinningが有効になっています
JDK1.7では、このパラメーターを削除し、組み込みの実装に変更します。
同期ブロックが非常に長く、スピンが失敗すると、システムのパフォーマンスが低下します。同期ブロックが非常に短く、スピンが成功した場合、スレッドサスペンションスイッチング時間を節約し、システムのパフォーマンスを向上させます。
2.4ポジティブロック、軽量ロック、スピンロックの概要
上記のロックは、Java言語レベルのロック最適化方法ではありませんが、JVMに組み込まれています。
まず、ロックのバイアスは、同じロックを繰り返し取得/解放するときに、スレッドのパフォーマンス消費を回避することです。同じスレッドがまだこのロックを取得した場合、ロックにバイアスをかけようとするときに同期ブロックに直接入力され、ロックを再度取得する必要はありません。
軽量ロックとスピンロックはどちらも、スレッドの中断が非常にリソースを消費する操作であるため、オペレーティングシステムレベルでのミューテックス操作への直接の呼び出しを回避することを目的としています。
ヘビー級ロック(オペレーティングシステムレベルでのミューテックス)の使用を避けるために、最初に軽量ロックを試します。軽量ロックは、CAS操作を使用してロックを取得しようとします。軽量ロックが取得できない場合、競争があることを意味します。しかし、おそらくあなたはすぐにロックを取得し、スピンロックを試して、スレッドでいくつかの空のループを実行し、ループするたびにロックを取得しようとします。スピンロックも失敗した場合、ヘビー級ロックにのみアップグレードできます。
バイアスロック、軽量ロック、スピンロックはすべて楽観的なロックであることがわかります。
3.ロックを誤って使用する場合
public class integerlock {static integer i = 0; public static class addthread extends thread {public void run(){for(int k = 0; k <100000; k ++){synchronized(i){i ++; }}}} public static void main(string [] args)throws arturnedexception {addthread t1 = new addThread(); addthread t2 = new addthread(); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); }}非常に基本的な間違いは、[高い並行性Java VII]並行性設計パターンでは、インターガーが最終的に変化していないことであり、各++の後、新しいインターガーが生成されてIに割り当てられ、2つのスレッド間で競合するロックが異なります。したがって、それはスレッドセーフではありません。
4。ThreadLocalとそのソースコード分析
ここでthreadlocalに言及するのは少し不適切かもしれませんが、Threadlocalはロックを置き換える方法です。それで、それについて言及する必要があります。
基本的な考え方は、マルチスレッドでは、データが矛盾する必要があるということです。 threadlocalを使用すると、各スレッドにオブジェクトインスタンスが提供されます。異なるスレッドは、他のオブジェクトではなく、独自のオブジェクトのみにアクセスします。このようにして、ロックが存在する必要はありません。
パッケージテスト; java.text.parseexceptipcectippectipceションのインポート; java.text.simpledateformat; import java.util.date; import java.util.date; import java.util.concurrent.executorservice; import java.util.concurrent.executors; public class test {private static final simpledateformateformateformateformateformation HH:mm:ss "); public static class parsedate実装は実行可能{int i = 0; public parsedate(int i){this.i = i; } public void run(){try {date t = sdf.parse( "2016-02-16 17:00:" + i%60); System.out.println(i + ":" + t); } catch(parseexception e){e.printstacktrace(); }}} public static void main(string [] args){executorservice es = executors.newfixedthreadpool(10); for(int i = 0; i <1000; i ++){es.execute(new parsedate(i)); }}} SimpleDateFormatはスレッドセーフではないため、上記のコードは誤って使用されます。最も簡単な方法は、クラスを自分で定義し、同期(Collections.synchronizedMapに似ています)でラップすることです。これは、高い並行性でそれを行うときに問題を引き起こします。同期した結果に関する競合は、一度に1つのスレッドのみが入力するだけで、並行性のボリュームは非常に低くなります。
この問題は、StreadLocalを使用してSimpleDateFormatをカプセル化することにより解決されます。
パッケージテスト; java.text.parseexception; import java.text.simpledateformat; import java.util.date; import java.util.concurrent.executorservice; import java.util.concurrent.executors; public class test {static thread <simpledatefformat> simpledateformat> usperdat> simpledateformat>( public static class parsedate実装は実行可能{int i = 0; public parsedate(int i){this.i = i; } public void run(){try {if(tl.get()== null){tl.set(new SimpledateFormat( "yyyy-mm-dd hh:mm:ss")); } date t = tl.get()。parse( "2016-02-16 17:00:" + i%60); System.out.println(i + ":" + t); } catch(parseexception e){e.printstacktrace(); }}} public static void main(string [] args){executorservice es = executors.newfixedthreadpool(10); for(int i = 0; i <1000; i ++){es.execute(new parsedate(i)); }}}各スレッドが実行されているとき、現在のスレッドにSimpleDateFormatオブジェクトがあるかどうかが決まります。
if(tl.get()== null)
そうでない場合、新しいSimpleDateFormatは現在のスレッドにバインドされます
tl.set(new SimpledateFormat( "yyyy-mm-dd hh:mm:ss"));
次に、現在のスレッドのSimpleDateFormatを使用して解析します
tl.get()。parse( "2016-02-16 17:00:" + i%60);
最初のコードでは、StreadLocalを使用したSimpleDateFormatは1つだけで、各スレッドに対してSimpleDateFormatが新しいものでした。
これは役に立たないため、ここで各スレッドローカルに公開されたsimpledateformatを設定しないでください。それぞれをsimpledateformatに新しいものにする必要があります。
Hibernateでは、threadlocalには典型的なアプリケーションがあります。
Threadlocalのソースコードの実装を見てみましょう
まず、スレッドクラスにはメンバー変数があります。
threadlocal.threadlocalmap threadlocals = null;
そして、このマップはThreadLocalの実装の鍵です
public void set(t value){thread t = thread.currentthread(); ThreadLocalMap Map = getMap(t); if(map!= null)map.set(this、value); else createmap(t、value); } ThreadLocalによると、対応する値を設定して取得できます。
ここでのThreadLocalMapの実装はハッシュマップに似ていますが、ハッシュ競合の処理には違いがあります。
ThreadLocalMapでハッシュ競合が発生した場合、リンクされたリストを使用して競合を解決するのはHashMapのようなものではなく、インデックス++を次のインデックスに配置して競合を解決します。