English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

java スレッド同期の詳細およびサンプルコード

java スレッド同期

概要:

コードの実行速度を速めるために、私たちはマルチスレッドの方法を採用しました。並行実行はコードをより効率的にするものですが、多くのスレッドがプログラム内で同時に動作しているため、これらのスレッドが同時に同じオブジェクトを変更する場合、誤りが発生する可能性があります。この場合、これらのスレッドを管理するための同期メカニズムを使用する必要があります。

(一)競合条件

オペレーティングシステムの中で、私に強く印象に残ったのは一つの図です。その図には、いくつかのプロセスが描かれており、その中でいくつかのスレッドが分かれています。これらのスレッドはすべて一斉にプロセスのリソースに向かっています。Javaでも同様に、リソースはスレッド間で共有され、各スレッドが独立したリソースを持っているわけではありません。この共有状況では、複数のスレッドが同時に同じリソースにアクセスすることがあり、この現象を競合条件と呼びます。

銀行システムでは、各スレッドがそれぞれの口座を管理しており、これらのスレッドが振替の操作を行うことがあります。
スレッドが操作を行う際、まず、口座残高をレジスタに保存します。次に、レジスタの数字から振り出す金額を減じます。最後に、結果を残高に書き戻します。
問題は、このスレッドが完了した後1、2一方のスレッドが別のスレッドがアカウント残高の値を変更するために唤醒され、しかし、この時点では最初のスレッドは知りません。最初のスレッドは第二のスレッドが終了するのを待ち、次の第三のステップ:結果を残高に書き戻します。この時点で、第二のスレッドの操作が消去されますので、システムの合計金額は間違いが発生します。
これがJAVAの競合条件が発生した悪い状況です。

(二)ReentrantLockクラス

上記の例では、私たちの操作が原子操作でない場合、中断されることは確実に起こります。時には確率が非常に低い場合もありますが、このような状況を排除することはできません。私たちのコードをオペレーティングシステムの原子操作のようにすることはできません。私たちができるのは、コードにロックをかけてセキュリティを確保することです。並行プログラムでは、データにアクセスする前に、コードにロックをかけて、ロックを使用する間、コードで使用されるリソースは「ロックされた」状態になり、他のスレッドにアクセスできなくなります。ロックを開くまでその状態が続きます。

JAVAでは、synchronizedキーワードとReentrantLockクラスがロック機能を持っています。まず、ReentrantLockの機能について一緒に話しましょう。

1.ReentrantLockコンストラクタ

このクラスでは、デフォルトのコンストラクタと、フェアな戦略を持つコンストラクタの2つを提供しています。このフェアな戦略は、まず通常のロックよりも非常に遅いです。次に、ある場合には実際にはフェアではない場合もあります。そして、特別な理由がない場合は、フェアな戦略を研究する必要はありません。

2.取得と解放

ReentrantLock myLock = new ReentrantLock();
//オブジェクトの作成
myLock.lock();
//ロックの取得
try{
...
}
finally{
myLock.unlock();
//ロックの解放
}

finallyブロックでロックを解放することを忘れないでください。以前に述べたように、未確認のエラーはスレッドの終了につながります。不思議なほどに終了すると、プログラムは下に進まなくなります。finallyブロックに解放を置かないと、このロックは常に解放されません。この理屈は、私たちが日常的にフレームワークで使用する包の.close()と同じです。closeについて言えば、言及すべきことは、ロックを使用する際には「リソースを持つtry文」を使用できないことです。リソースを持つtry文が何かを知らない場合は、私の言葉を聞かないでください。

3ロックは再入性を持ちます。

リCURSIVEまたはループプログラムでロックを使用する場合、安心して使用してください。ReentrantLockロックは再入性を持ち、lock()が呼び出されるたびに、呼び出し回数を記録するカウントを保持します。しかし、lock()の呼び出しのたびに、unlock()で解放する必要があります。

(三)条件オブジェクト

通常、スレッドがロックをかけた後、临界領域に入るときに問題が発生します。彼らが必要とするリソースが他のオブジェクト中被用いていたり、彼らが実行できる条件を満たしていない場合があります。この場合、条件オブジェクトを使用して、ロックを取得したが、役に立たない作業をしているスレッドを管理する必要があります。

if(a>b){
  a.set(b-1);
}

1「自分自身を困らせて」

これは非常にシンプルな条件判断ですが、並行プログラムではこのように書けません。問題は、このスレッドが判断を完了した直後に別のスレッドが唤醒され、そのスレッドが操作してaがb以下になる(if文の条件がもはや正しくない)場合です。

この時点で、if文全体をロックの中に入れることを考えます。自分のコードが割り込みを受け入れないようにします。しかし、これではまた問題があります。if判断がfalseの場合、if内の文は実行されません。しかし、if内の文を実行する必要がある場合、またはif判断が正しくなるまで待ってから実行する必要がある場合、突然、if文はもう正しくならないことに気づきます。なぜなら、私たちのロックがこのスレッドをロックしているため、他のスレッドは临界領域にアクセスできず、aとbの値を変更してif判断を正しくする方法がありません。これは非常に厄介です。私たちのロックが自分自身を困らせており、出られず、他の者が入れない状態です。

2.Conditionクラス

この状況を解決するために、ReentrantLockクラスのnewConditionメソッドを使用して条件オブジェクトを取得します。

Condition cd = myLock.newCondition();

Conditionオブジェクトを取得した後、このオブジェクトにどのようなメソッドがあり、どのような効果があるかを研究する必要があります。APIを見ることは急がないで、テーマに戻り、現在急いで解決しなければならないのはif条件の判断の問題です。どうすれば:ロックがかかっている状態で、if判断が誤っていることが判明した場合、他のスレッドに機会を与え、自分はif判断が正しくなるまで待ち続けます。

Conditionクラスはこの問題を解決するために生まれました。Conditionクラスがあれば、if文の次にawaitメソッドを直接続けます。このメソッドは、このスレッドがブロックされ、ロックを放棄し、他のスレッドが操作するのを待つことを示します。

ここで使用している名詞は「ブロック」です。私たちは以前にも言いましたが、ブロックと待機には大きな違いがあります:ロックを取得する際に、ロックが空きになったら自動的にロックを取得できますが、ブロックでロックを取得する場合、空きのロックがあっても、スレッドスケジューラがそのスレッドにロックを保持することを許可するまで待たなければなりません。

他們のスレッドはif文の内容が順調に実行された後、signalAllメソッドを呼び出す必要があります。このメソッドは、この条件でブロックされたすべてのスレッドを再び活性化し、これらのスレッドに再び機会を与えます。これらのスレッドは、以下から許可されています:ブロックされた場所では、その後の処理を続けますこの時点で、スレッドは条件を再びテストする必要があります。条件がまだ満たされない場合は、上記の手順を繰り返す必要があります。

ReentrantLock myLock = new ReentrantLock();
//ロックオブジェクトを作成してください
myLock.lock();
//以下のクリティカルセクションにロックをかけてください
Condition cd = myLock.newCondition();
//Conditionオブジェクトを作成し、このcdオブジェクトは条件オブジェクトを表します
while(!(a>b))
  cd.await();
//上記のwhileループとawaitメソッドの呼び出しは標準的な書き方です
//ifの条件を満たせない場合、ブロック状態に入り、ロックを放棄し、他の人が活性化するのを待ちます
a.set(b-1);
//whileループから退出し、判定条件を満たしたら、自分の機能を実行します
cd.signalAll();
//最後に、他のブロックされたスレッドを活性化するためにsignalAllメソッドを呼び出すことを忘れないでください
//すべてのスレッドが他のスレッドのsignalAllを待っていると、死锁状態に入ります

非常に厄介な状況ですが、すべてのスレッドが他のスレッドのsignalAllを待っていると、死锁状態に入ります。死锁状態とは、すべてのスレッドが必要とするリソースが他のスレッドによって環状構造で形成され、誰も実行できない状態です。最後にsignalAllメソッドを呼び出して、cdによってブロックされた他の「兄弟」を活性化することは、私たちにとって便利で、死锁の発生を減らすために必要です。

3.Conditionオブジェクトとロックの要約

要約すると、Conditionオブジェクトとロックには以下のような特徴があります。

  1. ロックはコードの一部を保護するために使用できます。一度に保護された領域に入れることができるのはスレッドが1つだけです
  2. ロックはクリティカルセクションに入ろうとするスレッドを管理できます
  3. ロックは1つまたは複数の条件オブジェクトを持つことができます
  4. 各条件オブジェクトは、先ほど説明した理由で実行できないがすでに保護されたコードセクションに入ったスレッドを管理します

(四)synchronizedキーワード

私たちが先ほど紹介したReentrantLockとConditionオブジェクトは、コードの一部を保護するための方法の一つです。Javaには他にも一つのメカニズムがあります:キーワードsynchronizedを使用してメソッドを修飾し、メソッドに内部ロックを追加します。バージョンから、Javaの各オブジェクトには内部ロックがあります。つまり、synchronized修飾されたメソッドを呼び出すには、まず内部オブジェクトロックを取得する必要があります。

1.synchronizedとReentrantLockの比較

私たちが上記のコードを出します:

public void function(){
  ReentrantLock myLock = new ReentrantLock();
  myLock.lock();
  Condition cd = myLock.newCondition();
  while(!(a>b))
    cd.await();
  a.set(b-1);
  cd.signalAll();
}

このコードをsynchronizedで実装すると、以下のようになります:

public synchronized void function(){
  while(!(a>b))
    wait();
  a.set(b-1);
  notifyAll();
}

注意すべきことは、synchronizedキーワードを使用する際には、ReentrantLockとConditionオブジェクトを使用する必要はありません。waitメソッドをawaitメソッドに、notifyAllメソッドをsignalAllメソッドに置き換えました。これにより、前よりも非常にシンプルになりました。

2.静的メソッドのsynchronized

静的メソッドをsynchronizedとして宣言することも合法です。このメソッドを呼び出すと、関連するクラスの内部ロックが取得されます。例えば、Testクラスの静的メソッドを呼び出すと、Test.classのロックがロックされます。

3.内部ロックと条件の制限

内部ロックは簡単ですが、多くの制限があります:

  1. ロックを取得しようとする中間のスレッドを中断することはできません
  2. ロックを取得しようとする際にタイムアウトを設定することはできません
  3. Conditionを通じて条件をインスタンス化することはできません。各ロックには単一の条件があり、それは十分ではないかもしれません

コードでどちらのロックを使用するべきか?LockとConditionオブジェクトか、同期メソッドか?core javaの本にはいくつかの提案があります:

  1. 最善の方法は、ReentrantLockもsynchronizedキーワードも使用しないことです。多くの場合、java.util.concurrentパッケージを使用できます
  2. synchronizedがあなたのコードの要件に合っている場合は、それを優先して使用してください
  3. 特別にReentrantLockが必要な場合にのみ、それを使用するまで待ちます

読んでいただきありがとうございます。皆様のサポートに感謝します!

おすすめ