web-dev-qa-db-ja.com

ConcurrentHashMapに格納されている原子的にインクリメントするカウンター

Webアプリのさまざまな場所からいくつかのメトリックを収集したいと思います。簡単にするために、これらはすべてカウンターになるため、修飾子の操作は1ずつインクリメントすることだけです。

増分は同時に、頻繁に行われます。読み取り(統計のダンプ)はまれな操作です。

ConcurrentHashMapを使用することを考えていました。問題は、カウンターを正しくインクリメントする方法です。マップには「インクリメント」操作がないため、最初に現在の値を読み取り、新しい値をマップに配置するよりもインクリメントする必要があります。これ以上のコードがなければ、これはアトミック操作ではありません。

同期なしでこれを達成することは可能ですか(これはConcurrentHashMapの目的を無効にします)? グアバ を見る必要がありますか?

ポインタをありがとう。


PS
SO( Javaでマップ値をインクリメントする最も効率的な方法 )に関連する質問がありますが、マルチスレッドではなくパフォーマンスに焦点を当てています

[〜#〜]更新[〜#〜]
同じトピックの検索でここに到着した人のために:以下の回答の他に、同じトピックを偶然にカバーする便利な プレゼンテーション があります。スライド24〜33を参照してください。

35

Java 8:

ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();

map.computeIfAbsent("key", k -> new LongAdder()).increment();
33
ZhekaKozlov

Guavaの新しい AtomicLongMap (リリース11)は、このニーズに対応する可能性があります。

19
Louis Wasserman

あなたはかなり近いです。 _ConcurrentHashMap<Key, AtomicLong>_のようなものを試してみませんか? Keys(メトリック)が変更されていない場合は、標準のHashMapを使用することもできます(読み取り専用の場合はスレッドセーフですが、これをImmutableMap Googleコレクションまたは_Collections.unmodifiableMap_などから)。

このように、map.get(myKey).incrementAndGet()を使用して統計をバンプできます。

8

AtomicLongを使用する以外に、通常のcas-loopを実行できます。

private final ConcurrentMap<Key,Long> counts =
    new ConcurrentHashMap<Key,Long>();

public void increment(Key key) {
    if (counts.putIfAbsent(key, 1)) == null) {
        return;
    }

    Long old;
    do {
       old = counts.get(key);
    } while (!counts.replace(key, old, old+1)); // Assumes no removal.
}

(私は何年もの間do-whileループを書いていません。)

値が小さい場合、Longはおそらく「キャッシュ」されます。より長い値の場合、割り当てが必要になる場合があります。ただし、割り当ては実際には非常に高速です(さらにキャッシュできます)。最悪の場合、予想する内容によって異なります。

LongAdderAtomicLongのパフォーマンスを比較するためのベンチマークを行いました。

LongAdderのベンチマークでは、パフォーマンスが向上しました。サイズ100(10の同時スレッド)のマップを使用した500回の反復では、LongAdderの平均時間は1270ミリ秒でしたが、AtomicLongの平均時間は1315ミリ秒でした。

1
f.ald

同じことをする必要がありました。 ConcurrentHashMap + AtomicIntegerを使用しています。また、ReentrantRW Lockがアトミックフラッシュ用に導入されました(非常によく似た動作)。

各キーごとに10個のキーと10個のスレッドでテストされています。何も失われませんでした。私はまだいくつかのフラッシュスレッドを試していませんが、うまくいくことを願っています。

大規模なシングルユーザーモードのフラッシュは私を苦しめています... RWLockを削除し、フラッシュを細かく分割したいと思います。明日。

private ConcurrentHashMap<String,AtomicInteger> counters = new ConcurrentHashMap<String, AtomicInteger>();
private ReadWriteLock rwLock = new ReentrantReadWriteLock();

public void count(String invoker) {

    rwLock.readLock().lock();

    try{
        AtomicInteger currentValue = counters.get(invoker);
        // if entry is absent - initialize it. If other thread has added value before - we will yield and not replace existing value
        if(currentValue == null){
            // value we want to init with
            AtomicInteger newValue = new AtomicInteger(0);
            // try to put and get old
            AtomicInteger oldValue = counters.putIfAbsent(invoker, newValue);
            // if old value not null - our insertion failed, lets use old value as it's in the map
            // if old value is null - our value was inserted - lets use it
            currentValue = oldValue != null ? oldValue : newValue;
        }

        // counter +1
        currentValue.incrementAndGet();
    }finally {
        rwLock.readLock().unlock();
    }

}

/**
 * @return Map with counting results
 */
public Map<String, Integer> getCount() {
    // stop all updates (readlocks)
    rwLock.writeLock().lock();
    try{
        HashMap<String, Integer> resultMap = new HashMap<String, Integer>();
        // read all Integers to a new map
        for(Map.Entry<String,AtomicInteger> entry: counters.entrySet()){
            resultMap.put(entry.getKey(), entry.getValue().intValue());
        }
        // reset ConcurrentMap
        counters.clear();
        return resultMap;

    }finally {
        rwLock.writeLock().unlock();
    }

}
0
Vitalii