同時プログラミングは、Javaプログラマーにとって最も重要なスキルの1つであり、習得するのが最も難しいスキルの1つです。プログラマーは、コンピューターの最も低い動作原則を深く理解する必要があり、同時に、プログラマーが明確な論理と細心の思考を持つ必要があり、効率的で安全で信頼できるマルチ読み取りの同時プログラムを書くことができます。このシリーズは、スレッド間調整の性質(待機、Notify、NotifyAll)から始まり、同期して揮発性があり、JDKが提供する各並行性ツールと基礎となる実装メカニズムを詳細に説明します。これに基づいて、その使用、ソースコードの実装、その背後にある原則など、java.util.concurrentパッケージのツールクラスをさらに分析します。この記事は、このシリーズの最初の記事であり、このシリーズの最も中心的な理論的部分です。これに基づいて、後続の記事を分析および説明します。
1。共有
データ共有は、スレッドの安全性の主な理由の1つです。すべてのデータがスレッドでのみ有効である場合、スレッドの安全性の問題はありません。これは、プログラミング時にスレッドの安全性を考慮する必要がないことが多い主な理由の1つです。ただし、マルチスレッドプログラミングでは、データ共有は避けられません。最も典型的なシナリオは、データベース内のデータです。データの一貫性を確保するために、通常、同じデータベースでデータを共有する必要があります。マスターとスレーブの場合でも、同じデータにアクセスします。マスターとスレーブは、アクセスとデータセキュリティの効率のために同じデータをコピーしているだけです。ここで、単純な例を介して複数のスレッドの下でデータを共有することによって引き起こされる問題を示します。
コードスニペット1:
パッケージcom.paddx.test.concurrent; public class sharedata {public static int count = 0; public static void main(string [] args){final sharedata data = new sharedata(); for(int i = 0; i <10; i ++){newスレッド(new runnable(){@override public void run(){// conconcurrencyの問題の可能性を増やすために1ミリ秒間一時停止します。 data.addcount(); system.out.print(count + "")}); } try {//メインプログラムは3秒間一時停止され、上記のプログラムの実行がthread.sleep(3000)が完了するようにします。 } catch(arturnedexception e){e.printstacktrace(); } system.out.println( "count =" + count); } public void addcount(){count ++; }}上記のコードの目的は、1つの操作を追加して1,000回カウントおよび実行することですが、ここでは10個のスレッドを介して実装され、各スレッドは100回実行され、通常の状況では1,000が出力されるはずです。ただし、上記のプログラムを実行すると、結果が当てはまらないことがわかります。特定の時間の実行結果は次のとおりです(各実行の結果は同じではない場合があり、時には正しい結果が得られる場合があります):
共有された変数操作では、多面的な環境でさまざまな予期しない結果が簡単に見られることがわかります。
2。相互除外
リソース相互除外とは、1人の訪問者のみが同時にアクセスできることを意味します。これはユニークで排他的です。通常、複数のスレッドが同時にデータを読み取ることを許可しますが、同時にデータを記述できるスレッドは1つだけです。したがって、通常、ロックを共有ロックと独占ロックに分割します。これは、読み取りロックと書き込みロックとも呼ばれます。リソースが相互に排他的でない場合、リソースが共有されていても、スレッドの安全性を心配する必要はありません。たとえば、不変のデータ共有の場合、すべてのスレッドはそれを読むことができるため、スレッドの安全性の問題は必要ありません。ただし、共有データの操作を作成するには、一般に相互除外が必要です。上記の例では、相互排除がないため、データ変更の問題が発生します。 Javaは、相互排除を確保するための複数のメカニズムを提供します。最も簡単な方法は、同期して使用することです。次に、上記のプログラムに同期して追加して実行します。
コードスニペット2:
パッケージcom.paddx.test.concurrent; public class sharedata {public static int count = 0; public static void main(string [] args){final sharedata data = new sharedata(); for(int i = 0; i <10; i ++){newスレッド(new runnable(){@override public void run(){// conconcurrencyの問題の可能性を増やすために1ミリ秒間一時停止します。 data.addcount(); system.out.print(count + "")}); } try {//メインプログラムは3秒間一時停止され、上記のプログラムの実行がthread.sleep(3000)が完了するようにします。 } catch(arturnedexception e){e.printstacktrace(); } system.out.println( "count =" + count); } / ***同期キーワードを追加* / public同期void addcount(){count ++; }}上記のコードが実行されたので、実行する回数に関係なく、最終結果は1000になることがわかります。
iii。原子性
原子性とは、データの操作を独立した不可分な全体として指します。言い換えれば、それは継続的で中断性のない操作です。データ実行の半分は、他のスレッドによって変更されません。 Atomicityを確保する最も簡単な方法は、オペレーティングシステムの命令です。つまり、1つの操作が一度に1つのオペレーティングシステム命令に対応する場合、間違いなく原子性を確保します。ただし、1つの命令で多くの操作を完了することはできません。たとえば、長い型操作の場合、それぞれ高位置と低い位置で動作するために、多くのシステムを複数の指示に分割する必要があります。たとえば、実際に使用する整数I ++の動作は、次の3つのステップに分割する必要があります。(1)整数iの値を読み取ります。 (2)iに1つの操作を追加します。 (3)結果をメモリに戻します。このプロセスは、マルチスレッドで発生する場合があります。
これが、コードセグメントの実行の結果が正しくない理由でもあります。この組み合わせ操作では、Javaでの同期やロックなどの原子性を確保するための最も一般的な方法は実装でき、コードセグメント2は同期して実装されます。ロックに加えて、CAS(比較とスワップ)には別の方法があります。つまり、データを変更する前に、前の値が一貫しているかどうかを比較します。それらが一貫している場合は、それらを変更し、矛盾している場合は再び実行されます。これは、ロック実装を最適化する原則でもあります。ただし、CASは一部のシナリオでは効果的ではない場合があります。たとえば、別のスレッドは最初に特定の値を変更し、次に元の値に変更します。この場合、CASは判断できません。
4。可視性
可視性を理解するには、JVMのメモリモデルを特定して理解する必要があります。 JVMのメモリモデルは、図に示すように、オペレーティングシステムに似ています。
この図から、各スレッドには独自の作業メモリがあることがわかります(CPUアドバンストバッファーに相当します。この目的は、ストレージシステムとCPUの速度差をさらに狭め、パフォーマンスを向上させることです)。共有変数の場合、スレッドがワーキングメモリの共有変数のコピーを読み取るたびに。書くとき、作業メモリ内のコピーの値を直接変更し、特定の時点でメインメモリの値とワーキングメモリを同期させます。これが引き起こす問題は、スレッド1が特定の変数を変更した場合、スレッド1によって共有変数に加えられた変更が表示されない可能性があることです。次のプログラムを通じて、目に見えない問題を示すことができます。
パッケージcom.paddx.test.concurrent; public class visibilitytest {private static boolean ready; private static int番号。 private static class readerthread extends thread {public void run(){try {thread.sleep(10); } catch(arturnedexception e){e.printstacktrace(); } if(!ready){system.out.println(ready); } system.out.println(number); }} private static class writerthread extends thread {public void run(){try {thread.sleep(10); } catch(arturnedexception e){e.printstacktrace(); } number = 100; Ready = true; }} public static void main(string [] args){new WriterThread()。start(); new ReaderThread()。start(); }}直感的には、このプログラムは100のみ出力され、すぐに値は印刷されません。実際、上記のコードを複数回実行すると、さまざまな結果が得られる可能性があります。ここに2つの実行の結果があります。
もちろん、この結果は視界のためにのみ可能であると言えます。書き込みスレッド(WriterThread)が準備が整った場合= trueに設定されると、ReaderThreadは変更された結果を確認できないため、Falseが印刷されます。 2番目の結果、つまり、書き込みスレッドの結果はif(!ready)の実行時に読み取られていませんが、system.out.println(reading)を実行すると、書き込みスレッドの実行の結果が読み取られます。ただし、この結果は、スレッドの代替実行によって引き起こされる場合があります。視認性は、Javaの同期または揮発性によって確保でき、特定の詳細を後続の記事で分析します。
5。シーケンス
パフォーマンスを向上させるために、コンパイラとプロセッサは命令を再注文する場合があります。並べ替えには3つのタイプがあります。
(1)コンパイラが最適化された並べ替え。コンパイラは、シングルスレッドプログラムのセマンティクスを変更せずに、ステートメントの実行順序を再スケジュールできます。
(2)命令レベルの並列性の並べ替え。最新のプロセッサは、命令レベルの並列テクノロジー(ICP)を使用して、複数の命令の実行を重複させます。データ依存関係がない場合、プロセッサはマシンの命令に対応するステートメントの実行順序を変更できます。
(3)メモリシステムの並べ替え。プロセッサはキャッシュと読み取り/書き込みバッファーを使用するため、ロードおよびストレージ操作が順調に実行されるようになります。
JSR 133の並べ替えの問題の説明を直接参照できます。
(1)(2)
まず、上の写真のソースコードパーツ(1)を見てみましょう。ソースコードから、命令1が最初に実行されるか、命令3が最初に実行されます。命令1が最初に実行された場合、R2は命令4に記述された値を表示しないでください。命令3が最初に実行された場合、R1は命令2で記述された値を表示しないでください。ただし、実行結果にはR2 == 2およびR1 == 1があります。これは「再注文」の結果です。上記の図(2)は、可能な法的編集の結果です。編集後、命令1と命令2の順序が交換される場合があります。したがって、R2 == 2とR1 == 1の結果が表示されます。同期または揮発性をJavaで使用して、順序を確保することもできます。
6つの要約
この記事では、Java Concurrentプログラミングの理論的根拠について説明し、いくつかのことについては、可視性、順序などのその後の分析で詳細に説明します。その後の記事については、この章の内容に基づいて説明します。上記のコンテンツをよく理解できれば、他の同時プログラミング記事を理解するか、毎日の同時プログラミング作業を理解することであろうと、それはあなたにとって大きな助けになると思います。