我有一种情况,我必须维护一个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
不是这里的最佳解决方案吗?List
一起使用的ConcurrentHashMap
是否有问题? ConcurrentHashMap
的计算方法(我认为没有必要,因为ReentrantLock
已经在做同样的工作)?答案 0 :(得分:2)
@Slaw在评论中提供的答案可以解决问题。我们让 instrumentList 实例以非同步方式转义,即访问/操作发生在列表上而没有任何同步。通过将副本传递给其他方法来解决此问题,就可以解决问题。
下面的代码行是发生此问题的地方
recordSaver.persist(instrumentList); instrumentList.clear();
在这里,我们允许 instrumentList 实例以非同步方式转义,即,将其传递到要对其执行操作的另一个类(recordSaver.persist),但我们也正在清除列表在下一行(在Aggregator类中),所有这些都是以非同步方式发生的。列表状态无法在记录保护程序中预测到……这是一个非常愚蠢的错误。
我们通过将 instrumentList 的克隆副本传递给recordSaver.persist(...)方法来解决此问题。这样, instrumentList.clear()不会影响recordSaver中可用于进一步操作的列表。
答案 1 :(得分:0)
我知道,您正在锁中使用ConcurrentHashMap的parallelStream
。我对Java 8+流支持不了解,但是快速搜索显示了这一点
基于这些信息(以及我的肠道驱动的并发错误检测器™),我猜测是,删除对parallelStream
的调用可能会提高代码的健壮性。另外,如@Slaw所述,如果所有instrumentMap
的用法都已由锁保护,则应使用普通的HashMap代替ConcurrentHashMap。
当然,由于您没有发布recordSaver
的代码,因此它也可能存在错误(不一定是与并发相关的错误)。特别是,您应确保从持久性存储中读取记录的代码(即用于检测记录丢失的代码)是安全,正确的,并且与系统的其余部分正确同步(最好使用健壮的代码) ,行业标准的SQL数据库)。
答案 2 :(得分:0)
这似乎是在不需要优化的情况下进行的尝试。在这种情况下,越少越好。在下面的代码中,仅使用了两个并发概念: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) {}
}
}