簡體   English   中英

缺少帶有鎖和ConcurrentHashMap的更新

[英]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();
    }
}

該解決方案幾乎可以在我們測試的每種情況下都可以正常工作,除非有時我們會看到一些記錄丟失了,即盡管將它們添加到了地圖中,但它們根本沒有保留。

我的問題是:

  1. 此代碼有什么問題?
  2. ConcurrentHashMap不是這里的最佳解決方案嗎?
  3. ConcurrentHashMap一起使用的List是否有問題?
  4. 我是否應該在這里使用ConcurrentHashMap的計算方法(我認為沒有必要,因為ReentrantLock已經在執行相同的工作)?

@Slaw在評論中提供的答案可以解決問題。 我們讓instrumentList實例以非同步方式轉義,即訪問/操作發生在列表上而沒有任何同步。 通過將副本傳遞給其他方法來解決此問題。

下面的代碼行是發生此問題的代碼

recordSaver.persist(instrumentList); instrumentList.clear();

在這里,我們允許InstrumentList實例以非同步的方式轉義,即,將其傳遞到要對其執行操作的另一個類(recordSaver.persist),但我們還在下一行(在Aggregator類中)清除列表,並所有這些都是以非同步的方式發生的。 列表狀態無法在記錄保護程序中預測到……這是一個非常愚蠢的錯誤。

我們通過將InstrumentList的克隆副本傳遞給recordSaver.persist(...)方法來解決此問題。 這樣, instrumentList.clear()不會影響recordSaver中可用於進一步操作的列表。

我知道,您正在鎖中使用ConcurrentHashMap的parallelStream 我不了解Java 8+流支持,但是快速搜索顯示,

  1. ConcurrentHashMap是一個復雜的數據結構,過去曾經有並發錯誤
  2. 並行流必須遵守復雜且記錄不充分的使用限制
  3. 您正在並行流中修改數據

根據這些信息(以及我的腸道驅動的並發錯誤檢測器™),我猜測,刪除對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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM