41

マルチスレッド環境でキーの複数の値を集約しています。キーは事前にわかっていません。私はこのようなことをするだろうと思った:

class Aggregator {
    protected ConcurrentHashMap<String, List<String>> entries =
                            new ConcurrentHashMap<String, List<String>>();
    public Aggregator() {}

    public void record(String key, String value) {
        List<String> newList =
                    Collections.synchronizedList(new ArrayList<String>());
        List<String> existingList = entries.putIfAbsent(key, newList);
        List<String> values = existingList == null ? newList : existingList;
        values.add(value);
    }
}

私が目にする問題は、このメソッドを実行するたびに、の新しいインスタンスを作成する必要がありArrayList、それを破棄することです(ほとんどの場合)。これは、ガベージコレクターの不当な悪用のようです。メソッドを使用せずにこの種の構造を初期化する、より優れたスレッドセーフな方法はありますsynchronizerecordputIfAbsentメソッドに新しく作成された要素を返さないという決定と、(いわば)必要とされない限りインスタンス化を延期する方法がないことに、私は少し驚いています。

4

7 に答える 7

47

Java 8 では、この正確な問題に対応するための API が導入され、1 行のソリューションが作成されました。

public void record(String key, String value) {
    entries.computeIfAbsent(key, k -> Collections.synchronizedList(new ArrayList<String>())).add(value);
}

Java 7 の場合:

public void record(String key, String value) {
    List<String> values = entries.get(key);
    if (values == null) {
        entries.putIfAbsent(key, Collections.synchronizedList(new ArrayList<String>()));
        // At this point, there will definitely be a list for the key.
        // We don't know or care which thread's new object is in there, so:
        values = entries.get(key);
    }
    values.add(value);
}

これは、ConcurrentHashMap.

特別なメソッドputIfAbsent(K, V))は、値オブジェクトを配置するか、別のスレッドが前に取得した場合、値オブジェクトを無視します。いずれにせよ、 への呼び出し後は、putIfAbsent(K, V))スレッドget(key)間で一貫性があることが保証されているため、上記のコードはスレッドセーフです。

唯一の無駄なオーバーヘッドは、他のスレッドが同じキーに対して同時に新しいエントリを追加した場合です:新しく作成された値を破棄することになるかもしれませんが、それはまだエントリがなく、競合がある場合にのみ発生ますスレッドが失われますが、これは通常はまれです。

于 2012-05-24T19:00:53.140 に答える
11

最後に、@Bohemian の回答を少し修正しました。彼が提案した解決策は、values変数をputIfAbsent呼び出しで上書きするため、以前と同じ問題が発生します。動作すると思われるコードは次のようになります。

    public void record(String key, String value) {
        List<String> values = entries.get(key);
        if (values == null) {
            values = Collections.synchronizedList(new ArrayList<String>());
            List<String> values2 = entries.putIfAbsent(key, values);
            if (values2 != null)
                values = values2;
        }
        values.add(value);
    }

私が望むほどエレガントではありませんが、ArrayList呼び出しごとに新しいインスタンスを作成するオリジナルよりは優れています。

于 2012-05-24T21:28:08.460 に答える
3

これは私も答えを探していた問題です。このメソッドputIfAbsentは、余分なオブジェクト作成の問題を実際に解決するわけではなく、これらのオブジェクトの 1 つが別のオブジェクトを置き換えないようにするだけです。ただし、スレッド間の競合状態により、複数のオブジェクトがインスタンス化される可能性があります。この問題に対する 3 つの解決策を見つけることができました (そして、この優先順位に従います)。

computeIfAbsent1- Java 8 を使用している場合、これを実現する最善の方法は、おそらくConcurrentMap. 同期的に実行される計算関数を与えるだけです(少なくともConcurrentHashMap実装のために)。例:

private final ConcurrentMap<String, List<String>> entries =
        new ConcurrentHashMap<String, List<String>>();

public void method1(String key, String value) {
    entries.computeIfAbsent(key, s -> new ArrayList<String>())
            .add(value);
}

これは、次の javadoc からのものですConcurrentHashMap.computeIfAbsent

指定されたキーがまだ値に関連付けられていない場合、指定されたマッピング関数を使用してその値を計算し、null でない限りこのマップに入力しようとします。メソッド呼び出し全体がアトミックに実行されるため、関数はキーごとに最大 1 回適用されます。他のスレッドがこのマップに対して試行した更新操作の一部は、計算の進行中にブロックされる可能性があるため、計算は短く単純にする必要があり、このマップの他のマッピングを更新しようとしてはなりません。

2- Java 8 を使用できない場合は、スレッドセーフなGuava'sを使用できます。LoadingCacheロード関数を (compute上記の関数と同様に) 定義すると、確実に同期的に呼び出されます。例:

private final LoadingCache<String, List<String>> entries = CacheBuilder.newBuilder()
        .build(new CacheLoader<String, List<String>>() {
            @Override
            public List<String> load(String s) throws Exception {
                return new ArrayList<String>();
            }
        });

public void method2(String key, String value) {
    entries.getUnchecked(key).add(value);
}

3- Guava も使用できない場合は、いつでも手動で同期し、ダブルチェック ロックを実行できます。例:

private final ConcurrentMap<String, List<String>> entries =
        new ConcurrentHashMap<String, List<String>>();

public void method3(String key, String value) {
    List<String> existing = entries.get(key);
    if (existing != null) {
        existing.add(value);
    } else {
        synchronized (entries) {
            List<String> existingSynchronized = entries.get(key);
            if (existingSynchronized != null) {
                existingSynchronized.add(value);
            } else {
                List<String> newList = new ArrayList<>();
                newList.add(value);
                entries.put(key, newList);
            }
        }
    }
}

これら 3 つのメソッドすべての実装例を作成し、さらに非同期メソッドを作成しました。これにより、余分なオブジェクトが作成されます: http://pastebin.com/qZ4DUjTr

于 2016-07-22T14:58:58.423 に答える