前の記事では、Java8を使用してオブザーバーパターンを実装する方法を紹介しました(パート1)。この記事では、Java8オブザーバーパターンに関する関連する知識を引き続き紹介しています。特定のコンテンツは次のとおりです。
スレッドセーフ実装
前の章では、現代のJava環境におけるオブザーバーパターンの実装を紹介します。それは単純ですが、完全ですが、この実装は重要な問題、つまりスレッドの安全性を無視します。ほとんどのオープンJavaアプリケーションはマルチスレッドであり、オブザーバーモードは主にマルチスレッドまたは非同期システムで使用されます。たとえば、外部サービスがデータベースを更新する場合、アプリケーションはメッセージを非同期に受信し、外部サービスを直接登録および聴く代わりに、オブザーバーモードで内部コンポーネントに更新するように通知します。
オブザーバーモードのスレッドの安全性は、主にモードの本体に焦点を当てています。これは、登録されたリスナーコレクションを変更するときにスレッドの競合が発生する可能性が高いためです。たとえば、1つのスレッドは新しいリスナーを追加しようとしますが、もう1つのスレッドは新しい動物オブジェクトを追加しようとします。これにより、登録されたすべてのリスナーに通知がトリガーされます。シーケンスの順序を考えると、最初のスレッドは、登録されたリスナーが追加の動物の通知を受け取る前に、新しいリスナーの登録を完了した場合とそうでない場合があります。これは、スレッドリソース競争の古典的なケースであり、スレッドの安全性を確保するためのメカニズムが必要であることを開発者に伝えるのはこの現象です。
この問題の最も簡単な解決策は、登録リスナーリストにアクセスまたは変更するすべての操作が、次のようなJava同期メカニズムに従う必要があることです。
Public Synchronized AnimalAddedListener RegistanimalAddedListener(AnimalAddedListenerリスナー){/*.....*/} Public Synchronized UnregisteranimalAddedListener(AnimalAddedListenerリスナー){/*...// /*...*/}このようにして、同時に、登録されたリスナーリストを変更またはアクセスできるスレッドのみがリソースの競争の問題を回避できますが、新しい問題が発生し、そのような制約は厳格すぎます(同期されたキーワードとJavaの同意モデルの詳細については、公式のWebページを参照してください)。メソッドの同期により、リスナーリストへの同時アクセスを常に観察できます。リスナーを登録して取り消すことは、リスナーリストの書き込み操作です。リスナーにリスナーリストにアクセスするように通知することは、読み取り専用操作です。通知によるアクセスは読み取り操作であるため、複数の通知操作を同時に実行できます。
したがって、登録されていない限り、登録されているリスナーリストのリソース競争をトリガーすることなく、任意の数の同時通知を同時に実行できる限り、登録が登録されていない限り、リスナーの登録または取り消しがない限り。もちろん、他の状況でのリソース競争は長い間存在してきました。この問題を解決するために、ReadWritelockのリソースロックは、読み取り操作を個別に管理するように設計されています。動物園クラスのスレッドセーフスレッドセーフェズー実装コードは次のとおりです。
public class threadsafezoo {private final readwritelock readwritelock = new reintrantreadwritelock();保護された最終ロックreadlock = readwriteLock.readLock();最終的なロックwritelock = readwriteLock.writeLock();プライベートリスト<動物>動物= new ArrayList <>(); private list <AnimalAddedListener>リスナー= new ArrayList <>(); public void addanimal(動物){//動物のリストに動物を追加します。リスナーshis.notifyAnimaladdedlisteners(animal);} public AntimalAddedListener RegistAnimalAddedListener(AnimalAddedListenerリスナー){//これを書くためにリスナーのリストをロックしてください。 {//ライターのロックを解除してくださいlocks.writeLock.unlock();} returnリスナー;} public void unregisteranimaladdedlistener(animalAddedListenerリスナー){//リスナーのリストをロックしてthis.writeLock.lock(); trea listensthis.listeners.remove(リスナー);}最後に{//ライターのロックを解除してくださいlockthis.writeLock.unlock();}} public void notifyAnimaladdedlisteners(Animal Animal){//リスナーのリストをロックします。 listensthis.listeners.foreach(ristener-> ristener.updateanimaladded(animal));} fultiant {//読者locks.readlock.unlock();}}}}}のロックを解除するこのような展開を通じて、サブジェクトの実装により、スレッドの安全性と複数のスレッドが同時に通知を発行できるようになります。しかし、それにもかかわらず、無視できない2つのリソース競争の問題がまだあります。
各リスナーへの同時アクセス。複数のスレッドは、リスナーに新しい動物が必要であることを通知できます。つまり、リスナーは複数のスレッドで同時に呼び出される場合があります。
動物リストへの同時アクセス。複数のスレッドは、動物リストにオブジェクトを同時に追加する場合があります。通知の順序が影響を与える場合、リソース競争につながる可能性があります。これには、この問題を回避するために同時の操作処理メカニズムが必要です。登録されたリスナーリストがAnimal2を追加する通知を受け取り、その後、Animal1を追加する通知を受け取ると、リソース競争が発生します。ただし、動物1と動物2の添加が異なるスレッドで実行される場合、動物2の前に動物1の添加を完了することもできます。具体的には、スレッド1はリスナーに通知する前に動物1を追加し、モジュールをロックし、スレッド2は動物2を追加してリスナーに通知し、スレッド1はリスナーにAnimal1が追加されたことを通知します。シーケンスの順序が考慮されない場合、リソース競争は無視できますが、問題は現実です。
リスナーへの同時アクセス
同時アクセスリスナーは、リスナーのスレッドの安全性を確保することで実装できます。クラスの「自己責任」の精神を順守して、リスナーは独自のスレッドの安全性を確保する「義務」を持っています。たとえば、上記のリスナーの場合、複数のスレッドで動物の数を増やすか減少させると、スレッドの安全性の問題につながる可能性があります。この問題を回避するには、動物数の計算は原子操作(原子変数またはメソッド同期)でなければなりません。特定のソリューションコードは次のとおりです。
パブリッククラスのスレッドセーチャティンティンティンガレッドドリステナーは動物addedListenerを実装します{private static atimiclong animalsdedcount = new atomiclong(0);@overridepublic void updateanimaladded(動物){//動物系の数を増分します。メソッド同期ソリューションコードは次のとおりです。
パブリッククラスCountingAnimalAddedListener ImmilationAddedListener {private static int AnimalsAddedCount = 0; @OverridePublic同期UpdateAnimalAdded(動物動物){//動物系の数の増加。リスナーは独自のスレッドの安全性を確保する必要があることを強調する必要があります。被験者は、リスナーにアクセスして変更するためのスレッドの安全性を確保するのではなく、リスナーの内部ロジックを理解する必要があります。それ以外の場合、複数の被験者が同じリスナーを共有する場合、各サブジェクトクラスはスレッドセーフコードを書き換える必要があります。明らかに、そのようなコードは十分に簡潔ではないため、リスナークラスにスレッドセーフを実装する必要があります。
リスナーの注文通知
リスナーが整然とした方法で実行する必要がある場合、読み取りおよび書き込みロックはニーズを満たすことができず、通知関数の呼び出し順序が動物にZooに追加される順序と一致するようにするために、新しいメカニズムを導入する必要があります。一部の人々は、メソッドの同期を使用してそれを実装しようとしましたが、Oracleドキュメントでのメソッド同期の導入によれば、メソッドの同期は操作実行の順序管理を提供しないことがわかります。原子動作が中断されないことのみを保証し、最初のCome-First実行(FIFO)のスレッド順序を保証しません。 ReentrantreadWriteLockは、そのような実行命令を実装できます。コードは次のとおりです。
public class orderedthreadsafezoo {private final readwritelock readwritelock = new reintrantreadwritelock(true);保護された最終ロックreadlock = readwriteLock.readLock();最終的なロックwritelock = readwriteLock.writeLock();プライベートリスト<動物>動物= new ArrayList <>(); private list <AnimalAddedListener>リスナー= new ArrayList <>(); public void addanimal(動物){//動物のリストに動物を追加します。リスナーshis.notifyAnimaladdedlisteners(animal);} public AntimalAddedListener RegistAnimalAddedListener(AnimalAddedListenerリスナー){//これを書くためにリスナーのリストをロックしてください。 {//ライターのロックを解除してくださいlocks.writeLock.unlock();} returnリスナー;} public void unregisteranimaladdedlistener(animalAddedListenerリスナー){//リスナーのリストをロックしてthis.writeLock.lock(); trea listensthis.listeners.remove(リスナー);}最後に{//ライターのロックを解除してくださいlockthis.writeLock.unlock();}} public void notifyAnimaladdedlisteners(Animal Animal){//リスナーのリストをロックします。 listensthis.listeners.foreach(ristener-> ristener.updateanimaladded(animal));} fultiant {//読者locks.readlock.unlock();}}}}}のロックを解除するこのようにして、登録、登録、および通知関数は、ファーストインファーストアウト(FIFO)の順序で読み取りおよび書き込みロック権限を取得します。たとえば、スレッド1はリスナーを登録し、スレッド2は登録操作を開始した後、登録リスナーに通知を試みます。スレッド2が読み取り専用ロックを待っているときに登録リスナーに通知を試みます。これにより、実行順序とアクションの開始順序が一貫していることが保証されます。
メソッドの同期が採用されている場合、スレッド2は最初にリソースを占有するためにキューアップしますが、スレッド3はスレッド2の前にリソースロックを取得することができ、スレッド2がスレッド3よりもリスナーに最初に通知することを保証することはできません。問題の鍵は次のとおりです。読み取りおよび書き込みロックの順序メカニズムは非常に複雑です。ロックのロジックが問題を解決するのに十分であることを確認するために、ReentrantreadWritelockの公式ドキュメントを参照する必要があります。
これまでにスレッドの安全性が実装されており、トピックのロジックを抽出し、そのミックスインクラスを再現可能なコードユニットにカプセル化することの利点と欠点が次の章で紹介されます。
Mixinクラスにカプセル化されたテーマロジック
上記のオブザーバーパターン設計の実装をターゲットミックスクラスにカプセル化することは非常に魅力的です。一般的に言えば、オブザーバーモードのオブザーバーには、登録されたリスナーのコレクションが含まれています。新しいリスナーの登録を担当する関数を登録します。登録された登録されたUnregister機能を取り消す責任のあるUnregister機能と、リスナーに通知する責任のある関数に通知します。動物園の上記の例では、動物クラスの他のすべての操作は、問題に動物リストが必要であることを除き、主題の論理を実装することです。
混合クラスの場合を以下に示します。コードをより簡潔にするために、スレッドの安全性に関するコードがここで削除されることに注意する必要があります。
パブリッククラスObservablesubjectMixin <siendertype> {private list <siendertype> listeners = new ArrayList <>(); public regrainType RegisterListener(ListenerTypeリスナー){//登録済みのリスナーのリストにリスナーを追加します。リスナー){//登録されたリスナーのリストからリスナーを削除します。listeners.lemove(リスナー);} public void notifyListeners(Consumer <?super riendertype> algorithm){//各リスナーshissis.listeners.foreach(algorithm);}}}}登録されたリスナータイプのインターフェイス情報は提供されていないため、特定のリスナーに直接通知することはできないため、通知関数の普遍性を確保し、一般的なパラメータータイプのパラメーターマッチングを受け入れるなど、クライアントが各リスナーに適用できるようにするなど、クライアントがいくつかの関数を追加できるようにする必要があります。特定の実装コードは次のとおりです。
Public class ZoousingMixinは、ObservablesubjectMixin <AnimalAddedListener> {private list <nimallist = new ArrayList <>(); public void addanimal(Animal Animal){//動物を動物のリストに追加するgions.notifelistenersのリストを拡張します。 listeners.updateanimaladded(動物);}}Mixinクラステクノロジーの最大の利点は、各件名クラスのロジックを繰り返すのではなく、オブザーバーパターンの主題を繰り返し可能なクラスにカプセル化することです。さらに、この方法により、動物園クラスの実装がより簡単になり、リスナーを保存して通知する方法を考慮せずに動物情報のみを保存します。
ただし、Mixinクラスを使用することは単なる利点ではありません。たとえば、複数のタイプのリスナーを保存する場合はどうなりますか?たとえば、リスナータイプAnimalRemovedListenerを保存する必要もあります。 Mixinクラスは抽象クラスです。 Javaでは、複数の抽象クラスを同時に継承することはできません。また、代わりにインターフェイスを使用してMixinクラスを実装することはできません。これは、インターフェイスに状態が含まれておらず、オブザーバーモードの状態を使用して登録されたリスナーリストを保存する必要があるためです。
解決策の1つは、動物が増加して減少したときに通知されるリスナータイプのZoolistenerを作成することです。コードは次のようになります:
public interface zoolistener {public void onanimaladded(動物動物); public void onanimalremoved(動物動物);}このようにして、このインターフェイスを使用して、リスナータイプを使用して動物園のさまざまな変更の監視を実装できます。
Public Class ZoousingMixinは、ObservablesUbjectMixin <Zoolistener> {private list <nimallist = new Arraylist <>(); public void addanimal(Animal Animal){//動物を動物のリストに追加するAnimals.add(動物); listerer.onanimaladded(animal));} public void removeanimal(動物動物){//動物のリストから動物を削除します。複数のリスナータイプを1つのリスナーインターフェイスに組み合わせると、上記の問題が解決しますが、まだ欠点があります。これについては、次の章で詳しく説明します。
マルチメソッドリスナーとアダプター
上記の方法では、リスナーインターフェイスがあまりにも多くの関数を実装している場合、インターフェイスはあまりにも冗長になります。たとえば、Swing Mouselistenerには5つの必要な関数が含まれています。そのうちの1つだけを使用できますが、マウスクリックイベントを使用している限り、これらの5つの機能を追加する必要があります。空の関数本体を使用して残りの関数を実装する可能性が高く、間違いなくコードに不必要な混乱をもたらすでしょう。
解決策の1つは、アダプターを作成することです(コンセプトは、GOFによって提案されたアダプターパターンに由来します)。リスナーインターフェイスの操作は、特定のリスナークラスを継承するための抽象関数の形で実装されます。このようにして、特定のリスナークラスは、必要な関数を選択し、アダプターが必要としない関数にデフォルト操作を使用できます。たとえば、上記の例のZoolistenerクラスでは、ZooAdapterの作成(アダプターの命名ルールはリスナーと一致しているため、クラス名のリスナーをアダプターに変更するだけです)、コードは次のとおりです。
パブリッククラスのZooAdapterはZoolistener {@OverridePublic void onanimaladded(動物動物){} @OverridePublic void onanimalremoved(動物動物){}}}}一見、このアダプタークラスは取るに足らないものですが、それがもたらす利便性を過小評価することはできません。たとえば、次の特定のクラスでは、それらに役立つ関数を選択してください。
パブリッククラスnameprinterzooadapterはzooadapterを拡張します{@overridepublic void onanimaladded(動物動物){//追加された動物の名前を印刷しました。アダプタークラスの関数も実装できる2つの選択肢があります。1つはデフォルト関数を使用することです。もう1つは、リスナーインターフェイスとアダプタークラスを特定のクラスにマージすることです。デフォルト関数はJava 8によって新たに提案されており、開発者はインターフェイスでデフォルトの(防御)実装方法を提供できるようにします。
Javaライブラリのこの更新は、主にコードの古いバージョンを変更せずに開発者がプログラム拡張機能を実装するように促進するため、この方法は注意して使用する必要があります。何度も使用した後、一部の開発者は、このように記述されたコードは十分に専門的ではないと感じるでしょう。これはJava 8の特徴であると考えています。デフォルト関数を使用して実装されたZoolistenerインターフェイスコードは次のとおりです。
public interface zoolistener {default public void onanimaladded(動物動物){}デフォルトパブリックボイドonanimalremoved(動物動物){}}デフォルト関数を使用することにより、インターフェイスの特定のクラスを実装すると、インターフェイスにすべての関数を実装する必要がありませんが、代わりに必要な関数を選択的に実装します。これは、インターフェイスの拡張の問題に対する比較的単純なソリューションですが、開発者はそれを使用する際にもっと注意を払う必要があります。
2番目のソリューションは、オブザーバーモードを簡素化し、リスナーインターフェイスを省略し、特定のクラスを使用してリスナーの関数を実装することです。たとえば、Zoolistenerインターフェイスが次のようになります。
Public Class Zoolistener {public void onanimaladded(動物動物){} public void onanimalremoved(動物動物){}}このソリューションは、オブザーバーパターンの階層を簡素化しますが、リスナーインターフェイスが特定のクラスにマージされた場合、特定のリスナーが複数のリスニングインターフェイスを実装できないため、すべてのケースには適用できません。たとえば、AnimalAddedListenerとAnimalRemovedListenerインターフェイスが同じコンクリートクラスで記述されている場合、単一の特定のリスナーは両方のインターフェイスを同時に実装できません。さらに、リスナーインターフェイスの意図は、特定のクラスの意図よりも明白です。前者が他のクラスにインターフェイスを提供することは明らかですが、後者はそれほど明白ではありません。
適切なドキュメントがなければ、開発者は、インターフェイスの役割を演じるクラスがすでにあることを知りません。すべての対応する機能を実装します。さらに、クラス名は特定のインターフェイスに収まらないため、クラス名にはアダプターが含まれていないため、クラス名はこの意図を特に意味しません。要約すると、特定の問題には特定の方法を選択する必要があり、方法は全能ではありません。
次の章を開始する前に、特にJavaコードの古いバージョンでは、アダプターが観測モードで一般的であることに言及することが重要です。 Swing APIは、Java 5およびJava 6のオブザーバーパターンで多くの古いアプリケーションが使用しているため、アダプターに基づいて実装されます。動物園ケースのリスナーはアダプターを必要としない場合がありますが、既存のコードで使用できるため、アダプターとそのアプリケーションの目的を理解する必要があります。次の章では、時間をかけたリスナーを紹介します。このタイプのリスナーは、時間のかかる操作を実行したり、非同期コールを行いたり、すぐに返品値を与えることができません。
複雑でブロックリスナー
オブザーバーパターンに関する1つの仮定は、関数が実行されると、一連のリスナーが呼び出されることですが、このプロセスは発信者に対して完全に透明であると想定されています。たとえば、クライアントコードが動物園で動物を追加する場合、返品が成功する前に一連のリスナーが呼び出されることは知られていません。リスナーの実行に長い時間がかかる場合(その時間がリスナーの数、各リスナーの実行時間の影響を受けます)、クライアントコードは、この単純な動物操作の単純な増加のタイム副作用を認識します。
この記事は、このトピックについて包括的な方法で議論することはできません。以下は、開発者が複雑なリスナーを呼び出すときに注意を払う必要があるものです。
リスナーは新しいスレッドを起動します。新しいスレッドが開始された後、新しいスレッドでリスナーロジックを実行すると、リスナー関数の処理結果が返され、他のリスナーが実行されます。
件名は新しいスレッドを開始します。登録されたリスナーリストの従来の線形反復とは異なり、SubjectのNotify関数は新しいスレッドを再起動し、新しいスレッドのリスナーリストを繰り返します。これにより、Notify関数は他のリスナー操作を実行しながら、リターン値を出力できます。リスナーリストが同時修正を受けないようにするには、スレッド安全メカニズムが必要であることに注意する必要があります。
キューリスナーは、スレッドのセットでリスニング関数を呼び出して実行します。一部の機能でリスナー操作をカプセル化し、リスナーのリストへの単純な反復コールの代わりにキューをキューにします。これらのリスナーがキューに保存されると、スレッドはキューから単一の要素をポップし、リスニングロジックを実行できます。これは、プロデューサー消費者の問題に似ています。 Notifyプロセスは、実行可能機能のキューを生成し、スレッドがキューを順番に取り出し、これらの機能を実行します。この関数は、リスナー関数が呼び出すために実行された時間ではなく、作成された時間を保存する必要があります。たとえば、リスナーが呼び出されたときに作成された関数では、関数は時点を保存する必要があります。この関数は、Javaの次の操作に似ています。
Public Class AnimalAddedFunctor {プライベートファイナルアニマルアデッドリステナーリスナー;プライベートファイナルアニマルパラメーター; public AntimalAddedFunctor(AnimalAddedListenerリスナー、動物パラメーター){this.listener =リスナー; this.parameter = parameter;} public void execute() creationthis.listener.updateanimaladded(this.parameter);}}関数はキューに作成および保存され、いつでも呼び出すことができるため、リストのリストのリストを横断するときにすぐに対応する操作を実行する必要はありません。リスナーをアクティブにする各関数がキューに押し込まれると、「消費者スレッド」がクライアントコードに運用上の権利を返します。 「消費者スレッド」は、リスナーがNotify関数によってアクティブ化されているかのように、後でこれらの機能を実行します。このテクノロジーは、他の言語でのパラメーターバインディングと呼ばれ、上記の例に適合しています。テクノロジーの本質は、リスナーのパラメーターを保存し、execute()関数を直接呼び出すことです。リスナーが複数のパラメーターを受信した場合、処理方法は類似しています。
リスナーの実行順序を保存する場合は、包括的なソートメカニズムを導入する必要があることに注意する必要があります。スキーム1では、リスナーは通常の順序で新しいスレッドをアクティブにします。これにより、リスナーが登録順に実行されます。スキーム2では、キューはソートをサポートし、それらの関数はキューに入る順序で実行されます。簡単に言えば、開発者はリスナーのマルチスレッド実行の複雑さに注意を払い、必要な関数を実装するために慎重に処理する必要があります。
結論
1994年にオブザーバーモデルが本に書き込まれる前は、すでに主流のソフトウェア設計モデルであり、ソフトウェアデザインでしばしば発生する問題に対する多くの満足のいくソリューションを提供しています。 Javaは常にこのパターンを使用するリーダーであり、このパターンを標準ライブラリにカプセル化していますが、Javaがバージョン8に更新されたことを考えると、古典的なパターンの使用を再検討する必要があります。ラムダの表現やその他の新しい構造の出現により、この「古い」パターンは新しい活力をもたらしました。古いプログラムを処理している場合でも、この長年の方法を使用して新しい問題を解決している場合でも、特に経験豊富なJava開発者にとって、オブザーバーパターンが開発者の主なツールです。
ONEAPMは、エンドツーエンドのJavaアプリケーションパフォーマンスソリューションを提供します。すべての一般的なJavaフレームワークとアプリケーションサーバーをサポートして、システムのボトルネックをすばやく発見し、異常の根本原因を見つけるのに役立ちます。瞬間的に展開して即座に展開すると、Javaの監視はかつてないほど容易になりました。その他の技術記事を読むには、Oneapmの公式テクノロジーブログをご覧ください。
上記のコンテンツは、Java8を使用してオブザーバーモード(パート2)を実装する方法を紹介します。すべての人に役立つことを願っています。