[英]Implementing a double buffered java HashMap that doesn't synchronize reads
所以我以為我有這個天才的想法來解決一個非常具體的問題,但是我無法擺脫最后一個潛在的線程安全問題。 我想知道你們是否有解決此問題的想法。
問題:
需要從僅很少更新的HashMap中讀取大量線程。 問題在於,在ConcurrentHashMap(即線程安全版本)中,由於write方法仍然鎖定bin(即映射的某些部分),因此read方法仍然有可能碰到互斥鎖。
這個想法:
有2個隱藏的HashMap充當一個……一個用於線程不同步地讀取,另一個用於線程寫入(當然具有同步),並且不時地翻轉它們。
顯而易見的警告是,地圖最終只會保持一致,但讓我們假設這足以滿足其預期目的。
但是出現的問題是,即使使用AtomicInteger等,它仍然保持一個競爭條件打開,因為僅在發生翻轉時,我不能確定讀者沒有溜進去……問題出在startRead()方法中的第262-272行和flip()方法中的第241-242行。
顯然,ConcurrentHashMap是用於解決此問題的非常好的類,我只想看看我是否可以進一步推廣該想法。
有人有想法么?
這是該類的完整代碼。 (尚未完全調試/測試,但是您知道了...)
package org.nectarframework.base.tools;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* This map is intended to be both thread safe, and have (mostly) non mutex'd
* reads.
*
* HOWEVER, if you insert something into this map, and immediately try to read
* the same key from the map, it probably won't give you the result you expect.
*
* The idea is that this map is in fact 2 maps, one that handles writes, the
* other reads, and every so often the two maps switch places.
*
* As a result, this map will be eventually consistent, and while writes are
* still synchronized, reads are not.
*
* This map can be very effective if handling a massive number of reads per unit
* time vs a small number of writes per unit time, especially in a massively
* multithreaded use case.
*
* This class isn't such a good idea because it's possible that between
* readAllowed.get() and readCounter.increment(), the flip() happens,
* potentially sending one or more threads on the Map that flip() is about to
* update. The solution would be an
* AtomicInteger.compareGreaterThanAndIncrement(), but that doesn't exist.
*
*
* @author schuttek
*
*/
public class DoubleBufferHashMap<K, V> implements Map<K, V> {
private Map<K, V> readMap = new HashMap<>();
private Map<K, V> writeMap = new HashMap<>();
private LinkedList<Triple<Operation, Object, V>> operationList = new LinkedList<>();
private AtomicBoolean readAllowed = new AtomicBoolean(true);
private AtomicInteger readCounter = new AtomicInteger(0);
private long lastFlipTime = System.currentTimeMillis();
private long flipTimer = 3000; // 3 seconds
private enum Operation {
Put, Delete;
}
@Override
public int size() {
startRead();
RuntimeException rethrow = null;
int n = 0;
try {
n = readMap.size();
} catch (RuntimeException t) {
rethrow = t;
}
endRead();
if (rethrow != null) {
throw rethrow;
}
return n;
}
@Override
public boolean isEmpty() {
startRead();
RuntimeException rethrow = null;
boolean b = false;
try {
b = readMap.isEmpty();
} catch (RuntimeException t) {
rethrow = t;
}
endRead();
if (rethrow != null) {
throw rethrow;
}
return b;
}
@Override
public boolean containsKey(Object key) {
startRead();
RuntimeException rethrow = null;
boolean b = false;
try {
b = readMap.containsKey(key);
} catch (RuntimeException t) {
rethrow = t;
}
endRead();
if (rethrow != null) {
throw rethrow;
}
return b;
}
@Override
public boolean containsValue(Object value) {
startRead();
RuntimeException rethrow = null;
boolean b = false;
try {
b = readMap.containsValue(value);
} catch (RuntimeException t) {
rethrow = t;
}
endRead();
if (rethrow != null) {
throw rethrow;
}
return b;
}
@Override
public V get(Object key) {
startRead();
RuntimeException rethrow = null;
V v = null;
try {
v = readMap.get(key);
} catch (RuntimeException t) {
rethrow = t;
}
endRead();
if (rethrow != null) {
throw rethrow;
}
return v;
}
@Override
public synchronized V put(K key, V value) {
operationList.add(new Triple<>(Operation.Put, key, value));
writeMap.put(key, value);
return value;
}
@Override
public synchronized V remove(Object key) {
// Not entirely sure if we should return the value from the read map or
// the write map...
operationList.add(new Triple<>(Operation.Delete, key, null));
V v = writeMap.remove(key);
endRead();
return v;
}
@Override
public synchronized void putAll(Map<? extends K, ? extends V> m) {
for (K k : m.keySet()) {
V v = m.get(k);
operationList.add(new Triple<>(Operation.Put, k, v));
writeMap.put(k, v);
}
checkFlipTimer();
}
@Override
public synchronized void clear() {
writeMap.clear();
checkFlipTimer();
}
@Override
public Set<K> keySet() {
startRead();
RuntimeException rethrow = null;
Set<K> sk = null;
try {
sk = readMap.keySet();
} catch (RuntimeException t) {
rethrow = t;
}
endRead();
if (rethrow != null) {
throw rethrow;
}
return sk;
}
@Override
public Collection<V> values() {
startRead();
RuntimeException rethrow = null;
Collection<V> cv = null;
try {
cv = readMap.values();
} catch (RuntimeException t) {
rethrow = t;
}
endRead();
if (rethrow != null) {
throw rethrow;
}
return cv;
}
@Override
public Set<java.util.Map.Entry<K, V>> entrySet() {
startRead();
RuntimeException rethrow = null;
Set<java.util.Map.Entry<K, V>> se = null;
try {
se = readMap.entrySet();
} catch (RuntimeException t) {
rethrow = t;
}
endRead();
if (rethrow != null) {
throw rethrow;
}
endRead();
return se;
}
private void checkFlipTimer() {
long now = System.currentTimeMillis();
if (this.flipTimer > 0 && now > this.lastFlipTime + this.flipTimer) {
flip();
this.lastFlipTime = now;
}
}
/**
* Flips the two maps, and updates the map that was being read from to the
* latest state.
*/
@SuppressWarnings("unchecked")
private synchronized void flip() {
readAllowed.set(false);
while (readCounter.get() != 0) {
Thread.yield();
}
Map<K, V> temp = readMap;
readMap = writeMap;
writeMap = temp;
readAllowed.set(true);
this.notifyAll();
for (Triple<Operation, Object, V> t : operationList) {
switch (t.getLeft()) {
case Delete:
writeMap.remove(t.getMiddle());
break;
case Put:
writeMap.put((K) t.getMiddle(), t.getRight());
break;
}
}
}
private void startRead() {
if (!readAllowed.get()) {
synchronized (this) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
readCounter.incrementAndGet();
}
private void endRead() {
readCounter.decrementAndGet();
}
}
我強烈建議您學習如何使用JMH ,這是在優化算法和數據結構的路徑上應該學習的第一件事。
例如,如果您知道如何使用它,則可以快速發現只有10%的寫入時ConcurrentHashMap
性能非常接近未同步的HashMap
。
4個線程(10%寫入):
Benchmark Mode Cnt Score Error Units
SO_Benchmark.concurrentMap thrpt 2 69,275 ops/s
SO_Benchmark.usualMap thrpt 2 78,490 ops/s
8個線程(10%寫入):
Benchmark Mode Cnt Score Error Units
SO_Benchmark.concurrentMap thrpt 2 93,721 ops/s
SO_Benchmark.usualMap thrpt 2 100,725 ops/s
使用較小的寫入百分比, ConcurrentHashMap
的性能往往會更接近HashMap
的性能。
現在,我修改了startRead
和endRead
,使它們無法運行,但是非常簡單:
private void startRead() {
readCounter.incrementAndGet();
readAllowed.compareAndSet(false, true);
}
private void endRead() {
readCounter.decrementAndGet();
readAllowed.compareAndSet(true, false);
}
讓我們看一下性能:
Benchmark Mode Cnt Score Error Units
SO_Benchmark.concurrentMap thrpt 10 98,275 ? 2,018 ops/s
SO_Benchmark.doubleBufferMap thrpt 10 80,224 ? 8,993 ops/s
SO_Benchmark.usualMap thrpt 10 106,224 ? 4,205 ops/s
這些結果表明,在每個操作上使用一個原子計數器和一個原子布爾修改,我們無法獲得比ConcurrentHashMap
更好的性能。 (我嘗試了30,10和5%的寫入,但是使用DoubleBufferHashMap
從來沒有帶來更好的性能)
如果您有興趣,請使用基准測試Pastebin 。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.