Webアプリのさまざまな場所からいくつかのメトリックを収集したいと思います。簡単にするために、これらはすべてカウンターになるため、修飾子の操作は1ずつインクリメントすることだけです。
増分は同時に、頻繁に行われます。読み取り(統計のダンプ)はまれな操作です。
ConcurrentHashMapを使用することを考えていました。問題は、カウンターを正しくインクリメントする方法です。マップには「インクリメント」操作がないため、最初に現在の値を読み取り、新しい値をマップに配置するよりもインクリメントする必要があります。これ以上のコードがなければ、これはアトミック操作ではありません。
同期なしでこれを達成することは可能ですか(これはConcurrentHashMapの目的を無効にします)? グアバ を見る必要がありますか?
ポインタをありがとう。
PS
SO( Javaでマップ値をインクリメントする最も効率的な方法 )に関連する質問がありますが、マルチスレッドではなくパフォーマンスに焦点を当てています
[〜#〜]更新[〜#〜]
同じトピックの検索でここに到着した人のために:以下の回答の他に、同じトピックを偶然にカバーする便利な プレゼンテーション があります。スライド24〜33を参照してください。
Java 8:
ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();
map.computeIfAbsent("key", k -> new LongAdder()).increment();
Guavaの新しい AtomicLongMap (リリース11)は、このニーズに対応する可能性があります。
あなたはかなり近いです。 _ConcurrentHashMap<Key, AtomicLong>
_のようなものを試してみませんか? Key
s(メトリック)が変更されていない場合は、標準のHashMap
を使用することもできます(読み取り専用の場合はスレッドセーフですが、これをImmutableMap
Googleコレクションまたは_Collections.unmodifiableMap
_などから)。
このように、map.get(myKey).incrementAndGet()
を使用して統計をバンプできます。
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
はおそらく「キャッシュ」されます。より長い値の場合、割り当てが必要になる場合があります。ただし、割り当ては実際には非常に高速です(さらにキャッシュできます)。最悪の場合、予想する内容によって異なります。
LongAdder
とAtomicLong
のパフォーマンスを比較するためのベンチマークを行いました。
LongAdder
のベンチマークでは、パフォーマンスが向上しました。サイズ100(10の同時スレッド)のマップを使用した500回の反復では、LongAdderの平均時間は1270ミリ秒でしたが、AtomicLongの平均時間は1315ミリ秒でした。
同じことをする必要がありました。 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();
}
}