同時プログラミングの目的は、プログラムの実行をより速く実行することですが、同時性を使用すると、必ずしもプログラムがより速く実行されるとは限りません。同時プログラミングの利点は、同時プログラムの数が一定の程度に達する場合にのみ反映できます。したがって、同時性が高い場合、同時プログラミングについて話すことは意味があります。並行性量の高いプログラムはまだ開発されていませんが、同時性を学習することは、分散アーキテクチャをよりよく理解することです。次に、シングルスレッドプログラムなど、プログラムの並行性ボリュームが高くない場合、シングルスレッドの実行効率はマルチスレッドプログラムの実行効率よりも高くなります。なぜこれがなぜですか?オペレーティングシステムに精通している人は、CPUが各スレッドに時間スライスを割り当てることによりマルチスレッドを実装することを知っておく必要があります。このようにして、CPUがあるタスクから別のタスクに切り替わると、前のタスクの状態が保存されます。タスクが実行されると、CPUは以前のタスクの状態を引き続き実行します。このプロセスは、コンテキストスイッチングと呼ばれます。
Java MultiThreadingでは、揮発性キーワードの同期キーワードが重要な役割を果たします。それらはすべてスレッドの同期を実装できますが、それはどのように下部に実装されていますか?
揮発性
揮発性は、各スレッドに対する変数の可視性を確保することができますが、原子性を保証することはできません。 Java言語の揮発性をどのように使用するかについてはあまり言いません。私の提案は、パッケージjava.util.concurrent.atomicのクラスライブラリを除く他の状況でそれを使用することです。詳細については、この記事を参照してください。
導入
次のコードを参照してください
パッケージorg.go; public class go {volatile int i = 0; private void inc(){i ++; } public static void main(string [] args){go go = new go(); for(int i = 0; i <10; i ++){new Thread(() - > {for(int j = 0; j <1000; j ++)go.inc();})。start(); } while(thread.activecount()> 1){thread.yield(); } system.out.println(go.i); }}上記のコードの各実行の結果は異なり、出力数は常に10000未満です。これは、Inc()を実行するとき、I ++は原子動作ではないためです。おそらく、Synchronized Inc()を使用するか、パッケージの下でロックの下のロックを使用してjava.util.concurrent.locksを使用してスレッドの同期を制御することを提案する人もいるでしょう。しかし、それらは次の解決策ほど良くありません。
パッケージorg.go; Import java.util.concurrent.atomic.atomicinteger; public class go {atomicinteger i = new AtomicInteger(0); private void inc(){i.getandincrement(); } public static void main(string [] args){go go = new go(); for(int i = 0; i <10; i ++){new Thread(() - > {for(int j = 0; j <1000; j ++)go.inc();})。start(); } while(thread.activecount()> 1){thread.yield(); } system.out.println(go.i); }}現時点では、Atomicの実装を理解していない場合、基礎となるAtomicIntegerがロックを使用して実装される可能性があるため、効率的でない可能性があることは間違いなく疑わしいでしょう。正確には、見てみましょう。
原子クラスの内部実装
AtomicIntegerであろうと同時に、ノードクラスconcurrentlinkedqueue.nodeであろうと、それらは静的変数を持っています
プライベート静的最終sun.misc.unsafe Unsafe;、このクラスは、原子セマンティクスを実装するSun :: of Sun :: of sunのJavaカプセル化です。基礎となる実装を見たいです。 GCC4.8のソースコードを手元に置いています。ローカルパスと比較して、GitHubへのパスを見つけるのは非常に便利です。ここを見てください。
インターフェイスgetAndincrement()の実装例を取得します
Atomicinteger.java
Private Static Final Unsafe Unsafe = unsafe.getunsafe(); public final int getandincrement(){for(;;){int current = get(); int next = current + 1; if(compareandset(current、next))return current; }} public final boolean Compareandset(int expect、int update){return unsafe.compareandswapint(this、value offset、expect、update); }これに注意してくださいループは、比較と成功した場合にのみ返されます。それ以外の場合は、常に比較します。
比較アンドセットの実装が呼び出されます。ここでは、Oracle JDKの実装がわずかに異なることに気付きました。 JDKの下でSRCを見ると、Oracle JDKがUnsafe getAndincrement()を呼び出すことがわかりますが、Oracle JDKがUnsafe.javaを実装する場合、比較対象は増加、減少、および設定の原子操作を実装できるため、比較対象と呼び出す必要があると思います。
Unsafe.java
パブリックネイティブブールアンドスワピント(オブジェクトOBJ、ロングオフセット、int expect、int update);
JNIを介して呼び出されるC ++の実装。
natunsafe.cc
jbooleansun :: misc :: unsafe :: compareandswapint(jobject obj、jlong offset、jint expect、jint update){jint *addr =(jint *)((char *)obj + offset); return Compareandswap(addr、expect、update);} static inline boolcompareandswap(volatile jint *addr、jint old、jint new_val){jboolean result = false;スピンロックロック; if((result =( *addr == old))) *addr = new_val; return result;} Unsafe :: CompareAndSwapintは、静的関数CampareAndSwapを呼び出します。 CompareAndSwapは、スピンロックをロックとして使用します。ここのスピンロックには、建設中にロックされ、破壊中に放出されるロックガードの意味があります。
スピンロックに集中する必要があります。 SpinLockがリリースされる前に、SpinLockが原子操作の真の実装であることを確認するためです。
スピンロックとは何ですか
SpinLock、リソースのロックを取得するのを待っているような忙しい。 Mutexが現在のスレッドをブロックし、必要なリソースを待つためにCPUリソースをリリースするのとは異なり、SpinLockは一時停止のプロセスに入り、条件が満たされるのを待ち、CPUを再競争させます。これは、ロックを待つコストがスレッド実行コンテキストスイッチのコストよりも少ない場合にのみ、スピンロックがミューテックスよりも優れていることを意味します。
natunsafe.cc
class spinlock {static volatile obj_addr_t lock; public:spinlock(){while(!compare_and_swap(&lock、0、1))_jv_threadield(); } 〜spinlock(){release_set(&lock、0); }};静的変数静的揮発性obj_addr_tロックを使用します。フラグとして、ガードがC ++ RAIIを介して実装されるため、いわゆるロックは実際には静的メンバー変数OBJ_ADDR_Tロックです。 C ++の揮発性は、同期を保証することはできません。保証されているのは、コンストラクターと静的変数ロックで呼ばれるcomparie_and_swapです。このロック変数が1の場合、待つ必要があります。 0の場合、原子動作を通じて1に設定し、ロックを取得したことを示します。
ここで静的変数を使用することは本当に事故です。つまり、すべてのロックフリー構造は、ロックを追加するかどうかを区別するために同じ変数(実際にsize_t)を共有することを意味します。この変数が1に設定されている場合、他のスピンロックを待つ必要があります。太陽にプライベート変数の揮発性obj_addr_tロックを追加してみませんか:: misc :: unsafe :: constructorパラメーターとしてスピンロックに渡しますか?これは、安全でないごとにフラグビットを共有することと同等です。効果は良くなりますか?
_jv_threadyield次のファイルでは、CPUリソースはSystem Call Sched_yield(Man 2 Sched_yield)を介して放棄されます。 Macro have_sched_yieldは構成で定義されています。つまり、コンパイル中に定義が未定義の場合、スピンロックは真のスピンロックと呼ばれます。
posix-threads.h
inline void_jv_threadyield(void){#ifdef have_sched_yield sched_yield();#endif / * have_sched_yield * /}このlock.hは、さまざまなプラットフォームで異なる実装を持っています。 IA64(Intel AMD X64)プラットフォームを例にとっています。他の実装はここで見ることができます。
IA64/locks.h
typedef size_t obj_addr_t; inline static boolcompare_and_swap(volatile obj_addr_t *addr、obj_addr_t old、obj_addr_t new_val){return __sync_bool_compare_and_swap(old、old、new voiddreme_seat *addr、obj_addr_t new_val){__asm__ __ volatile __( "" ::: "memory"); *(addr)= new_val;}__sync_bool_compare_and_swapは組み込みのGCC関数であり、アセンブリ命令「メモリ」がメモリバリアを完成させます。
要するに、ハードウェアはマルチコアCPUの同期を保証し、Unsafeの実装は可能な限り効率的です。 GCC-Javaは非常に効率的です。OracleとOpenJDKは悪化しないと思います。
原子運転とGCCビルトイン原子運転
原子操作
Java式とC ++式は原子操作ではありません。つまり、コードに含まれています。
//私がスレッド間で共有される変数I ++であると仮定します。
マルチスレッド環境では、私はアクセスが非原子的であり、実際には次の3つのオペランドに分かれています。
コンパイラは実行のタイミングを変更するため、実行結果が予想されない場合があります。
GCCビルトイン原子動作
GCCには、4.1.2から追加されたアトミック操作が組み込まれています。以前は、インラインアセンブリを使用して実装されていました。
タイプ__sync_fetch_and_add(type *ptr、type value、...)タイプ__sync_fetch_and_sub(type *ptr、type value、...)タイプ__sync_fetch_and_or(タイプ *ptr、タイプ値、...)タイプタイプ__sync_fetch_and_or(タイプ *ptr、type、type、type、typen value、...)タイプ__sync_fetch_and_xor(type *ptr、type value、...)タイプ__sync_fetch_and_nand(type *ptr、type value、...)タイプ__sync_add_and_fetch(タイプ *ptr、タイプ値、...)タイプ__sync_sub_sub_and_fetch(typen (タイプ *PTR、タイプ値、...)タイプ__sync_and_and_fetch(type *ptr、type value、...)タイプ__sync_and_and_fetch(type *ptr、type value、...)タイプ__sync_xor_and_fetch(type *ptr、type value、...)タイプ__nand_fetch(タイプ *) __sync_bool_compare_and_swap(type *ptr、type oldvalタイプNewval、...)タイプ__sync_val_compare_and_swap(type *ptr、type oldvalタイプnewval、...)__ sync_synchronize(...)タイプ__sync_lock_test_test_and_set(タイプ *ptr、価値、... ptr、valute、...) (タイプ *ptr、...)
注意すべきは、次のとおりです。
OpenJDK関連ファイル
以下は、GitHub上のOpenJDK9の原子操作の実装であり、知る必要がある人を助けることを望んでいます。結局のところ、OpenJDKはGCCよりも広く使用されています。 - しかし、結局のところ、Oracle JDKのソースコードはありませんが、OpenJDKとOracleの間のソースコードは非常に小さいと言われています。
Atomicinteger.java
unsafe.java::compareandexchangeObject
unsafe.cpp :: unsafe_compareandexchangeobject
ooop.inline.hpp :: oopdesc :: atomic_compare_exchange_oop
Atomic_linux_x86.hpp :: Atomic :: cmpxchg
インラインJlong Atomic :: CMPXCHG(Jlong Exchange_Value、volatile jlong Dest、jlong compare_value、cmmpxchg_memory_order order){bool mp = os :: is_mp(); __asm__ __volatile__(lock_if_mp(%4) "cmpxchgq%1、(%3)": "= a"(exchange_value): "r"(exchange_value)、 "a"(compare_value)、 "r"(dest)、 "r"(mp): "cc"); return exchange_value;}ここでは、C/C ++に精通していないJavaプログラマーにプロンプトを提供する必要があります。組み込みアセンブリの指示の形式は次のとおりです
__asm__ [__ volatile __](アセンブリテンプレート//アセンブリテンプレート:[出力オペランドリスト] //入力リスト:[入力オペランドリスト] //出力リスト:[Clobber List])//破壊リスト
アセンブリテンプレートの%1、%3、%4は、次のパラメーターリスト{"r"(Exchange_Value)、 "r"(dest)、 "r"(mp)}に対応し、パラメーターリストは0から分類され、出力パラメーターは最初のコロンの右側に配置され、出力パラメーターは右側に配置されます。 「R」とは一般的なレジスタに入れることを意味します。Aは登録EAXを意味し、「=」は出力(書き戻し)を意味します。 CMPXCHG命令は、EAXレジスタ、つまりパラメーター%2の使用を意味します。
他の詳細はここにはリストされません。 GCCの実装は、交換されるポインターを渡すことであり、比較が成功した後、値は直接割り当てられ(非原子を割り当てます)、原子性はSpinLockによって保証されます。
OpenJDKの実装は、交換されるポインターを渡し、アセンブリ命令CMMPXCHGQを通じて値を直接割り当てることであり、Atomicityはアセンブリ命令を通じて保証されます。もちろん、GCCのスピンロックの基礎となる層もCMMPXCHGQを通じて保証されています。
上記はこの記事のすべての内容です。みんなの学習に役立つことを願っています。誰もがwulin.comをもっとサポートすることを願っています。