4

スレッドセーフなリスナーを実装するとき、私は通常、どのタイプのCollectionリスナーを保持するのが最適かを考えます。これまでに 3 つのオプションを見つけました。

標準では、単純な へのアクセスをObservable使用します。リスナーのコピーを使用することはオプションですが、次のような問題を防ぐため、良いアイデアを伝えることができる限りsynchronizedArrayList

  • リスナーはコールバックで自分自身を削除します ( - それを防ぐために逆の順序でConcurrentModificationExceptionインデックス付きループを反復することができます)for
  • 同期ブロック内で外部コードを実行すると、全体がブロックされる可能性があります。

実装は残念ながら数行以上です。Collections.synchronizedList()は必要ありませんがsynchronizedremoveListenerそれほど価値はありません。

class ObservableList {
    private final List<Listener> listeners = new ArrayList<Listener>();
    public void addListener(Listener listener) {
        synchronized (listeners) {
            if (!listeners.contains(listener)) {
                listeners.add(listener);
            }
        }
    }
    public void removeListener(Listener listener) {
        synchronized (listeners) {
            listeners.remove(listener);
        }
    }
    protected void notifyChange() {
        Listener[] copyOfListeners;
        synchronized (listeners) {
            copyOfListeners = listeners.toArray(new Listener[listeners.size()]);
        }
        // notify w/o synchronization
        for (Listener listener : copyOfListeners) {
            listener.onChange();
        }
    }
}

しかし、本質的にスレッドセーフであり、より効率的である可能性があるCollectioninがあります。これは、それらの内部ロック メカニズムが単純なブロックjava.util.concurrentよりも最適化されていると想定しているためです。synchronized通知ごとにコピーを作成することも非常にコストがかかります。

に基づくCopyOnWriteArrayList

class ObservableCopyOnWrite {
    private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
    public void addListener(Listener listener) {
        listeners.addIfAbsent(listener);
    }

    public void removeListener(Listener listener) {
        listeners.remove(listener);
    }

    protected void notifyChange() {
        for (Listener listener : listeners) {
            listener.onChange();
        }
    }
}

最初のバージョンとほぼ同じようにする必要がありますが、コピーは少なくなります。リスナーの追加/削除は頻繁に行う操作ではないため、コピーも頻繁に行うべきではありません。

私が通常使用するバージョンは、次のように使用されるConcurrentHashMap状態に基づいています。.keySet()Set

ビューのイテレータは、ConcurrentModificationException を決してスローしない「弱い一貫性のある」イテレータであり、イテレータの構築時に存在していた要素をトラバースすることを保証し、構築後の変更を反映する場合があります (ただし保証はされません)。

つまり、反復には少なくとも、反復の開始時に登録されたすべてのリスナーが含まれ、反復中に追加された新しいリスナーも含まれる場合があります。削除されたリスナーについてはわかりません。私はこのバージョンが好きです。なぜなら、それはコピーではなく、実装が簡単だからCopyOnWriteArrayListです。

class ObservableConcurrentSet {
    private final Set<Listener> listeners = Collections.newSetFromMap(new ConcurrentHashMap<Listener, Boolean>());

    public void addListener(Listener listener) {
        listeners.add(listener);
    }

    public void removeListener(Listener listener) {
        listeners.remove(listener);
    }

    protected void notifyChange() {
        for (Listener listener : listeners) {
            listener.onChange();
        }
    }
}

ベースの実装は良いアイデアですか、ConcurrentHashMapそれともここで何かを見落としましたか? それとももっとCollection使いやすいものはありますか?

4

2 に答える 2

2

ConcurrentHashMap ベースの実装は良いアイデアですか、それともここで何かを見落としていましたか? または、使用するさらに優れたコレクションはありますか?

ConcurrentHashMapこの場合は問題ないようです。それは確かに使用するよりも優れていCollections.synchronizedList()ます。CHM イテレーターは、イテレーターがマップを歩いている間に追加された場合、マップ内の新しいエントリーを表示する場合と表示しない場合があります。また、反復中に古いエントリが削除された場合、古いエントリが表示される場合と表示されない場合があります。これらはどちらも、問題のノードがいつ追加/削除されたか、および反復子が配置されている場所に関する競合状態によるものです。ただし、反復子は、削除されていない限り、開始時にマップ内にあったアイテムを常に表示します。

于 2013-09-23T18:11:12.073 に答える
2

Java 1.1 以降、これらすべてのバリアントよりもパフォーマンスが優れているパターンがあります。AWTEventMulticasterクラスとそれがどのように機能するかを見てください。不変性によってスレッドセーフを提供するため、追加のオーバーヘッドはありません。リスナーがまったくない場合、またはリスナーが 1 つしかない場合でも、最も効率的な方法で処理できます。まあ、一般的に最も効率的なイベント配信を提供すると思います。

http://docs.oracle.com/javase/7/docs/api/java/awt/AWTEventMulticaster.htmlを参照してください。

独自のイベント タイプに対してこのようなパターンを実装する方法を知りたい場合は、このクラスのソース コードを調べてください。

残念なことに、Swing の開発者はこれを知らず、ひどい EventListenerList を作成して開発者を間違った道に導きました。

ちなみに、このパターンは、イベント配信中に追加されたリスナーが、追加前に発生した現在配信されているイベントを認識できないという問題も解決します。無料で。Java 1.1 以降

于 2013-09-23T17:12:35.660 に答える