前に書く
多くの場合、並行プログラミングでロック操作を行う場合、コードブロックのロック操作は本当に妥当ですか?最適化が必要な領域はありますか?
序文
" [High Concurrency]で、ロック方法を最適化すると、デッドロックが発生しました!!「この記事では、デッドロックが発生するときに必要な4つの条件を紹介しました。4つの条件が同時に満たされた場合にのみ、デッドロックが発生する可能性があります。その中で、リクエストをブロックして状態を維持するときに、すべてのリソースを一度に適用する方法を使用しました。たとえば、送金操作の完了プロセスでは、アカウントAとアカウントBを同時に申請し、両方のアカウントが正常に申請された後、送金操作を実行します。このうち、実装した転送方式では、アカウントAとアカウントBが同時に取得されるまでリソースを循環的に取得するエンドレスループを使用しており、以下にコアコードを示します。
//一次申请转出账户和转入账户,直到成功
while(!requester.applyResources(this, target)){
//循环体为空
;
}
ResourcesRequesterクラスのapplyResources()メソッドが非常に短い時間実行され、プログラムの同時実行性によって引き起こされる競合が大きくない場合、プログラムは、数回から数十回、同時に転送アカウントと転送アカウントを取得できます。の。
ただし、ResourcesRequesterクラスのapplyResources()メソッドの実行に時間がかかる場合、またはプログラムの同時実行性が原因で発生する競合が比較的大きい場合は、転送アカウントと転送アカウントを同時に取得するのに数千サイクルかかることがあります。 。これはCPUリソースを大量に消費するため、現時点ではこのソリューションは実行できません。
それで、このソリューションを最適化する方法はありますか?
問題分析
無限ループを使用して常にリソースを取得することには問題があるため、別の方法で考えてみましょう。スレッドが実行され、条件が満たされていないことが判明した場合、スレッドを待機状態にすることができますか?条件が満たされたときに、待機スレッドに再度実行するよう通知しますか?
つまり、スレッドが必要とする条件が満たされない場合は、スレッドを待機状態にし、スレッドが必要とする条件が満たされると、待機中のスレッドに再度実行を通知します。このようにして、プログラムがループで待機してCPUを消費するという問題を回避できます。
だから、質問が再び来ています!条件が満たされない場合、スレッドを待機させる方法は?条件が満たされたときに、スレッドをウェイクさせる方法は?
はい、これは問題です!しかし、この問題の解決も非常に簡単です。簡単に言えば、スレッドの待機および通知メカニズムを使用します。
スレッド待機および通知メカニズム
スレッドの待機および通知メカニズムを使用して、要求をブロックし、条件を維持するときに、ループでアカウントリソースを取得する問題を最適化できます。特定の待機および通知メカニズムを以下に示します。
実行中のスレッドは最初にミューテックスを取得します。スレッドが実行を継続するときに必要な条件が満たされない場合、ミューテックスは解放され、待機状態になります。スレッドが必要な条件を実行し続けると、待機中のスレッドに通知され、再取得されます相互に排他的なロック。
つまり、Javaはこの種のスレッド待機および通知メカニズムをサポートしていますか?実際、この質問は少しナンセンスですが、Java(そのような優れた(非常に強力な)言語)は確かにそれをサポートしており、実装は比較的簡単です。
Javaによるスレッド待機および通知メカニズムの実装
方法を実現する
実際、Java言語を使用してスレッド待機および通知メカニズムを実装する方法はたくさんあります。ここでは、1つの方法を簡単に列挙します。他の方法を自分で考えて実装することができます。わからない場合は、聞いてください!
Java言語では、スレッドの待機および通知メカニズムを実装する簡単な方法は、同期化された待機()、通知()、および通知すべて()メソッドを組み合わせて使用することです。
実施原則
同期ロックを使用する場合、同期保護コードブロック、つまりクリティカルセクションに入ることができるスレッドは1つだけです。スレッドがクリティカルセクションに入ると、他のスレッドがブロッキングキューに入り、待機します。このブロッキングキューと同期ミューテックスロックは1対1の関係にあります。つまり、ミューテックスロックは独立したブロッキングキューに対応します。
並行プログラミングでは、スレッドが同期されたミューテックスを取得しても、停止を継続するための条件を満たさない場合は、待機状態に入る必要があります。この時点で、Javaのwait()メソッドを使用して実現できます。wait()メソッドが呼び出されると、現在のスレッドはブロックされ、待機するための待機キューに入ります。wait()メソッドの呼び出しによって入力された待機キューは、ミューテックスの待機キューでもあります。さらに、スレッドが待機キューに入ると、取得したミューテックスを解放するため、他のスレッドはミューテックスを取得してからクリティカルセクションに入ることができます。全体のプロセスは下図のように表すことができます。
スレッド実行条件が満たされた場合、Javaが提供するnotify()およびnotifyAll()メソッドを使用して、ミューテックスロックキュー内のスレッドに通知できます。次の図を使用して、このプロセスを簡単に表すことができます。
ここでは、次の項目に注意する必要があります。
(1)notify()メソッドとnotifyAll()メソッドを使用してスレッドに通知する場合、notify()メソッドとnotifyAll()メソッドが呼び出されると、スレッドの実行条件は満たされますが、スレッドが実際に実行されると、条件が満たされなくなる場合があります。実行のためにクリティカルセクションに入った他のスレッドがあります。
(2)待機するためにwait()メソッドが呼び出されたときにミューテックスが解放されているため、通知されたスレッドが実行を継続する場合、ミューテックスを最初に取得する必要があります。
(3)wait()、notify()およびnotifyAll()メソッドで操作されるキューは、ミューテックスロックの待機キューです。同期がこのオブジェクトにロックされている場合は、this.wait()、this.notify()およびthis.notifyAll()メソッド。同期によってターゲットオブジェクトがロックされる場合は、target.wait()、target.notify()、およびtarget.notifyAll()メソッドを使用する必要があります。
(4)wait()、notify()、notifyAll()メソッドの呼び出しの前提は、対応するミューテックスが取得されていることです。つまり、wait()、notify()、notifyAll()メソッドはすべて同期メソッド内にあります。または、コードブロックで呼び出されます。同期メソッドの外側またはコードブロックの外側で3つのメソッドが呼び出された場合、またはロックされたオブジェクトがこれであり、ターゲットオブジェクトを使用して3つのメソッドが呼び出された場合、JVMはjava.lang.IllegalMonitorStateException例外をスローします。
具体的な実現
実装ロジック
実装する前に、次の問題も考慮する必要があります。
- 選択するミューテックス
前のプログラムでは、TansferAccountクラスにResourcesRequesterクラスのシングルトンオブジェクトがあるため、これをミューテックスとして使用できます。誰もがこの点を理解する必要があります。
- スレッドが転送操作を実行するための条件
転出アカウントも転入アカウントも割り当てられていません。
- スレッドはいつ待機状態に入りますか
スレッドが実行を継続し、必要な条件が満たされない場合は、待機状態に入ります。
- 実行待ちスレッドに通知するタイミング
スレッドがアカウントのリソースを解放すると、待機中のスレッドに実行を継続するよう通知されます。
要約すると、次のコアコードを描画できます。
while(不满足条件){
wait();
}
だから、質問が来ています!wait()メソッドがwhileループで呼び出されるのはなぜですか?wait()メソッドが戻ると、スレッド実行の条件が変更された可能性があります。つまり、以前の条件は満たされていますが、満たされていないため、条件が満たされているかどうかを再確認する必要があります。
実装コード
最適化されたResourcesRequesterクラスのコードを以下に示します。
public class ResourcesRequester{
//存放申请资源的集合
private List<Object> resources = new ArrayList<Object>();
//一次申请所有的资源
public synchronized void applyResources(Object source, Object target){
while(resources.contains(source) || resources.contains(target)){
try{
wait();
}catch(Exception e){
e.printStackTrace();
}
}
resources.add(source);
resources.add(targer);
}
//释放资源
public synchronized void releaseResources(Object source, Object target){
resources.remove(source);
resources.remove(target);
notifyAll();
}
}
ResourcesRequesterシングルトンオブジェクトを生成するResourcesRequesterHolderのコードを以下に示します。
public class ResourcesRequesterHolder{
private ResourcesRequesterHolder(){}
public static ResourcesRequester getInstance(){
return Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
private ResourcesRequester singleton;
Singleton(){
singleton = new ResourcesRequester();
}
public ResourcesRequester getInstance(){
return singleton;
}
}
}
転送操作を実行するクラスのコードを以下に示します。
public class TansferAccount{
//账户的余额
private Integer balance;
//ResourcesRequester类的单例对象
private ResourcesRequester requester;
public TansferAccount(Integer balance){
this.balance = balance;
this.requester = ResourcesRequesterHolder.getInstance();
}
//转账操作
public void transfer(TansferAccount target, Integer transferMoney){
//一次申请转出账户和转入账户,直到成功
requester.applyResources(this, target))
try{
//对转出账户加锁
synchronized(this){
//对转入账户加锁
synchronized(target){
if(this.balance >= transferMoney){
this.balance -= transferMoney;
target.balance += transferMoney;
}
}
}
}finally{
//最后释放账户资源
requester.releaseResources(this, target);
}
}
}
ご覧のとおり、プログラムで待機状態のスレッドに通知する場合、notify()メソッドの代わりにnotifyAll()メソッドが使用されます。notify()メソッドとnotifyAll()メソッドの違いは何ですか?
notify()メソッドとnotifyAll()メソッドの違い
- 通知()メソッド
待機キュー内のスレッドにランダムに通知します。
- notifyAll()メソッド
待機キュー内のすべてのスレッドに通知します。
実際の作業プロセスでは、特別な要件がない場合は、notifyAll()メソッドを使用してみてください。notify()メソッドの使用は危険であるため、特定のスレッドに通知されない可能性があります。
最後に書く
この記事が役に立った場合は、WeChatで「Binghe Technology」のWeChatアカウントを検索してフォローし、Bingheで同時実行プログラミングのテクニックを学んでください。
最後に、コンカレントプログラミングで習得する必要があるコアスキルナレッジマップを添付します。コンカレントプログラミングを学習するときは、回り道を避けてください。