web-dev-qa-db-ja.com

ConcurrentHashMap:「putIfAbsent」で余分なオブジェクトの作成を回避しますか?

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

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の新しいインスタンスを作成する必要があるということです。これは、ガベージコレクタの正当化されていない乱用のようです。 synchronizerecordメソッドを使用せずに、この種の構造を初期化するより良いスレッドセーフな方法はありますか? putIfAbsentメソッドが新しく作成された要素を返さないという決定と、それが(いわば)呼び出されない限りインスタンス化を延期する方法がないことには、少し驚いています。

38

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)はスレッド間で一貫していることが保証されているため、上記のコードはスレッドセーフです。

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

41
Bohemian

Java-8以降では、次のパターンを使用してマルチマップを作成できます。

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

ConcurrentHashMapドキュメント(一般規約ではありません)は、ArrayListが新しいキー用に作成されている間に更新を遅らせるというわずかな初期コストで、ArrayListがキーごとに1回だけ作成されることを指定しています。

http://docs.Oracle.com/javase/8/docs/api/Java/util/concurrent/ConcurrentHashMap.html#computeIfAbsent-K-Java.util.function.Function-

15
Peter

最後に、@ 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インスタンスを作成するオリジナルよりも優れています。

11

ジーンの答えに基づいて2つのバージョンを作成

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

public  static <K,V> void putIfAbsetMultiValueSet(ConcurrentMap<K,Set<V>> entries, K key, V value) {
    Set<V> values = entries.get(key);
    if (values == null) {
        values = Collections.synchronizedSet(new HashSet<V>());
        Set<V> values2 = entries.putIfAbsent(key, values);
        if (values2 != null)
            values = values2;
    }
    values.add(value);
}

うまくいきます

3
fracca

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

1- Java 8を使用している場合、これを達成するための最良の方法は、おそらくcomputeIfAbsentの新しい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);
}

これはConcurrentHashMap.computeIfAbsentのjavadocによるものです。

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

2- Java 8を使用できない場合は、スレッドセーフなGuavaLoadingCacheを使用できます。ロード関数を定義します(上記の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

3
Utku Özdemir

putIfAbsentを使用したアプローチは、実行時間が最も速く、競合の多い環境での「ラムダ」アプローチより2〜50倍高速です。 ラムダはこの「パワーロス」の背後にある理由ではありません。問題は、Java-9最適化前のcomputeIfAbsent内の強制的な同期です。

ベンチマーク:

import Java.util.Random;
import Java.util.concurrent.ConcurrentHashMap;
import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;
import Java.util.concurrent.TimeUnit;
import Java.util.concurrent.atomic.AtomicInteger;
import Java.util.concurrent.atomic.AtomicLong;

public class ConcurrentHashMapTest {
    private final static int numberOfRuns = 1000000;
    private final static int numberOfThreads = Runtime.getRuntime().availableProcessors();
    private final static int keysSize = 10;
    private final static String[] strings = new String[keysSize];
    static {
        for (int n = 0; n < keysSize; n++) {
            strings[n] = "" + (char) ('A' + n);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int n = 0; n < 20; n++) {
            testPutIfAbsent();
            testComputeIfAbsentLamda();
        }
    }

    private static void testPutIfAbsent() throws InterruptedException {
        final AtomicLong totalTime = new AtomicLong();
        final ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<String, AtomicInteger>();
        final Random random = new Random();
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);

        for (int i = 0; i < numberOfThreads; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    long start, end;
                    for (int n = 0; n < numberOfRuns; n++) {
                        String s = strings[random.nextInt(strings.length)];
                        start = System.nanoTime();

                        AtomicInteger count = map.get(s);
                        if (count == null) {
                            count = new AtomicInteger(0);
                            AtomicInteger prevCount = map.putIfAbsent(s, count);
                            if (prevCount != null) {
                                count = prevCount;
                            }
                        }
                        count.incrementAndGet();
                        end = System.nanoTime();
                        totalTime.addAndGet(end - start);
                    }
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
        System.out.println("Test " + Thread.currentThread().getStackTrace()[1].getMethodName()
                + " average time per run: " + (double) totalTime.get() / numberOfThreads / numberOfRuns + " ns");
    }

    private static void testComputeIfAbsentLamda() throws InterruptedException {
        final AtomicLong totalTime = new AtomicLong();
        final ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<String, AtomicInteger>();
        final Random random = new Random();
        ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
        for (int i = 0; i < numberOfThreads; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    long start, end;
                    for (int n = 0; n < numberOfRuns; n++) {
                        String s = strings[random.nextInt(strings.length)];
                        start = System.nanoTime();

                        AtomicInteger count = map.computeIfAbsent(s, (k) -> new AtomicInteger(0));
                        count.incrementAndGet();

                        end = System.nanoTime();
                        totalTime.addAndGet(end - start);
                    }
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
        System.out.println("Test " + Thread.currentThread().getStackTrace()[1].getMethodName()
                + " average time per run: " + (double) totalTime.get() / numberOfThreads / numberOfRuns + " ns");
    }

}

結果:

Test testPutIfAbsent average time per run: 115.756501 ns
Test testComputeIfAbsentLamda average time per run: 276.9667055 ns
Test testPutIfAbsent average time per run: 134.2332435 ns
Test testComputeIfAbsentLamda average time per run: 223.222063625 ns
Test testPutIfAbsent average time per run: 119.968893625 ns
Test testComputeIfAbsentLamda average time per run: 216.707419875 ns
Test testPutIfAbsent average time per run: 116.173902375 ns
Test testComputeIfAbsentLamda average time per run: 215.632467375 ns
Test testPutIfAbsent average time per run: 112.21422775 ns
Test testComputeIfAbsentLamda average time per run: 210.29563725 ns
Test testPutIfAbsent average time per run: 120.50643475 ns
Test testComputeIfAbsentLamda average time per run: 200.79536475 ns
1

空の配列リストの作成問題がJava 1.7.40。空の配列リストの作成について心配する必要はありません。参照: http:/ /javarevisited.blogspot.com.tr/2014/07/Java-optimization-empty-arraylist-and-Hashmap-cost-less-memory-jdk-17040-update.html

1