[英]Missing updates with locks and ConcurrentHashMap
我有一個場景,我必須維護一個可由多個線程填充的Map
,每個線程都修改各自的List
(唯一的標識符/鍵是線程名稱),並且當線程的列表大小超過固定的批處理大小時,我們必須將記錄持久化到數據庫中。
private volatile ConcurrentHashMap<String, List<T>> instrumentMap = new ConcurrentHashMap<String, List<T>>();
private ReentrantLock lock ;
public void addAll(List<T> entityList, String threadName) {
try {
lock.lock();
List<T> instrumentList = instrumentMap.get(threadName);
if(instrumentList == null) {
instrumentList = new ArrayList<T>(batchSize);
instrumentMap.put(threadName, instrumentList);
}
if(instrumentList.size() >= batchSize -1){
instrumentList.addAll(entityList);
recordSaver.persist(instrumentList);
instrumentList.clear();
} else {
instrumentList.addAll(entityList);
}
} finally {
lock.unlock();
}
}
每2分鍾(使用相同的鎖)每隔2分鍾就會運行一個單獨的線程來持久保存Map
所有記錄(以確保每2分鍾之后我們就可以持久保存一些內容,並且地圖的大小不會太大)
if(//Some condition) {
Thread.sleep(//2 minutes);
aggregator.getLock().lock();
List<T> instrumentList = instrumentMap.values().stream().flatMap(x->x.stream()).collect(Collectors.toList());
if(instrumentList.size() > 0) {
saver.persist(instrumentList);
instrumentMap .values().parallelStream().forEach(x -> x.clear());
aggregator.getLock().unlock();
}
}
該解決方案幾乎可以在我們測試的每種情況下都可以正常工作,除非有時我們會看到一些記錄丟失了,即盡管將它們添加到了地圖中,但它們根本沒有保留。
我的問題是:
ConcurrentHashMap
不是這里的最佳解決方案嗎? ConcurrentHashMap
一起使用的List
是否有問題? ConcurrentHashMap
的計算方法(我認為沒有必要,因為ReentrantLock
已經在執行相同的工作)? @Slaw在評論中提供的答案可以解決問題。 我們讓instrumentList實例以非同步方式轉義,即訪問/操作發生在列表上而沒有任何同步。 通過將副本傳遞給其他方法來解決此問題。
下面的代碼行是發生此問題的代碼
recordSaver.persist(instrumentList); instrumentList.clear();
在這里,我們允許InstrumentList實例以非同步的方式轉義,即,將其傳遞到要對其執行操作的另一個類(recordSaver.persist),但我們還在下一行(在Aggregator類中)清除列表,並所有這些都是以非同步的方式發生的。 列表狀態無法在記錄保護程序中預測到……這是一個非常愚蠢的錯誤。
我們通過將InstrumentList的克隆副本傳遞給recordSaver.persist(...)方法來解決此問題。 這樣, instrumentList.clear()不會影響recordSaver中可用於進一步操作的列表。
我知道,您正在鎖中使用ConcurrentHashMap的parallelStream
。 我不了解Java 8+流支持,但是快速搜索顯示,
根據這些信息(以及我的腸道驅動的並發錯誤檢測器™),我猜測,刪除對parallelStream
的調用可能會提高代碼的健壯性。 此外,如@Slaw所述,如果所有instrumentMap
用法都已由鎖保護,則應使用普通的HashMap代替ConcurrentHashMap。
當然,由於您沒有發布recordSaver
的代碼,因此它也可能存在錯誤(不一定是與並發相關的錯誤)。 特別是,您應確保從持久性存儲中讀取記錄的代碼(即用於檢測記錄丟失的代碼)是安全,正確的,並且與系統的其余部分正確同步的(最好使用健壯的代碼) ,行業標准的SQL數據庫)。
看起來這是對不需要的優化的嘗試。 在這種情況下,越少越好。 在下面的代碼,只有兩個並發概念用於: synchronized
,以確保共享列表正確地更新並final
以確保所有線程看到相同的值。
import java.util.ArrayList;
import java.util.List;
public class Aggregator<T> implements Runnable {
private final List<T> instruments = new ArrayList<>();
private final RecordSaver recordSaver;
private final int batchSize;
public Aggregator(RecordSaver recordSaver, int batchSize) {
super();
this.recordSaver = recordSaver;
this.batchSize = batchSize;
}
public synchronized void addAll(List<T> moreInstruments) {
instruments.addAll(moreInstruments);
if (instruments.size() >= batchSize) {
storeInstruments();
}
}
public synchronized void storeInstruments() {
if (instruments.size() > 0) {
// in case recordSaver works async
// recordSaver.persist(new ArrayList<T>(instruments));
// else just:
recordSaver.persist(instruments);
instruments.clear();
}
}
@Override
public void run() {
while (true) {
try { Thread.sleep(1L); } catch (Exception ignored) {
break;
}
storeInstruments();
}
}
class RecordSaver {
void persist(List<?> l) {}
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.