我有一个下面的类,我有一个add
方法,由另一个线程调用来填充我的clientidToTimestampHolder
多图。然后在同一个下面的类中,我启动一个后台线程,每60秒运行一次并调用processData()
方法,该方法迭代相同的映射并将所有这些数据发送到其他服务。
public class Handler {
private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private final Multimap<String, Long> clientidToTimestampHolder = ArrayListMultimap.create();
private static class Holder {
private static final Handler INSTANCE = new Handler();
}
public static Handler getInstance() {
return Holder.INSTANCE;
}
private Handler() {
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
processData();
}
}, 0, 60, TimeUnit.SECONDS);
}
// called by another thread to populate clientidToTimestampHolder map
public void add(final String clientid, final Long timestamp) {
clientidToTimestampHolder.put(clientid, timestamp);
}
// called by background thread
public void processData() {
for (Entry<String, Collection<Long>> entry : clientidToTimestampHolder.asMap().entrySet()) {
String clientid = entry.getKey();
Collection<Long> timestamps = entry.getValue();
for (long timestamp : timestamps) {
boolean isUpdated = isUpdatedClient(clientid, timestamp);
if (!isUpdated) {
updateClient(String.valueOf(clientid));
}
}
}
}
}
我的问题是,add
方法每次都会从不同的线程调用。那么我是否需要创建clientidToTimestampHolder
地图的副本并将该副本作为参数传递给processData()
方法,而不是直接在该地图上工作?
因为现在我使用相同的地图填充其中的数据,然后迭代相同的地图以将内容发送到其他服务,因此我不会删除该地图中的数据,因此这些条目将始终存在于该地图中。
解决此问题的最佳方法是什么?我需要确保它是线程安全的,并且没有竞争条件,因为我无法松开任何clientid
。
更新
所以我的processData
方法会是这样的吗?
public void processData() {
synchronized (clientidToTimestampHolder) {
Iterator<Map.Entry<String, Long>> i = clientidToTimestampHolder.entries().iterator();
while (i.hasNext()) {
String clientid = i.next().getKey();
long timestamp = i.next().getValue();
boolean isUpdated = isUpdatedClient(clientid, timestamp);
if (!isUpdated) {
updateClient(clientid);
}
i.remove();
}
}
}
答案 0 :(得分:2)
使用Multimaps.synchronized(List)Multimap
包装器对多地图进行线程安全引用(ArrayListMultimap
为ListMultimap
,即将值存储在列表中):
private final ListMultimap<String, Long> clientidToTimestampHolder =
Multimaps.synchronizedListMultimap(ArrayListMultimap.create());
请注意,同步多图包装器具有以下警告:
当访问任何集合视图时,用户必须手动同步返回的多图:
// ...
不遵循此建议可能会导致非确定性行为。
在您的情况下,您必须手动同步条目视图的迭代,因为它的迭代器没有同步:
public void processData() {
synchronized (clientidToTimestampHolder) {
for (Map.Entry<String, Long> entry : clientidToTimestampHolder.entries()) {
String clientid = entry.getKey();
long timestamp = entry.getValue();
boolean isUpdated = isUpdatedClient(clientid, timestamp);
if (!isUpdated) {
updateClient(String.valueOf(clientid));
}
}
clientidToTimestampHolder.clear();
}
}
(我使用Mutlimap.entries()
代替Multimap.asMap().entrySet()
,因为它更干净。
此外,如果您想知道为什么没有通用ConcurrentXxxMultimap
实施,请参阅Guava's issue #135和this comment quoting internal discussion about this:
我尝试构建一个通用的并发多图,然后就转了 在一小部分用途中稍快一点,速度要慢得多 在大多数用途中(与同步多图表相比)。我专注于 尽可能多地进行原子操作;一份较弱的合同会 消除这种缓慢的一些,但也会减损它 效用
我相信Multimap界面太大&#34;大&#34;支持一个 高效的并发实现 - 排序或其他。 (显然, 这是夸大其词,但至少需要一个 很多工作或松散Multimap界面。)
修改强>
阅读你的评论,对我来说似乎是XY Problem。话虽如此,IMO你不应该在这里使用Multimap
,因为你没有使用它的任何功能,而是采用BlockingQueue
方法({1}}方便({1}}并且是线程安全的):
private final LinkedBlockingQueue<Map.Entry<String, Long>> clientidToTimestampHolder =
new LinkedBlockingQueue<>();
public void add(final String clientid, final Long timestamp) {
clientidToTimestampHolder.offer(Maps.immutableEntry(clientid, timestamp));
}
public void processData() {
final List<Map.Entry<String, Long>> entries = new ArrayList<>();
clientidToTimestampHolder.drainTo(entries);
for (Map.Entry<String, Long> entry : entries) {
String clientid = entry.getKey();
long timestamp = entry.getValue();
boolean isUpdated = isUpdatedClient(clientid, timestamp);
if (!isUpdated) {
updateClient(String.valueOf(clientid));
}
}
}
您可以(应该?)为您的数据创建自己的值类,以存储String
和long
字段,并使用它而不是通用Map.Entry<String, Long>
。
答案 1 :(得分:0)
现在,使用您的代码,您将主要观察您的地图是不一致的,因为在一次迭代中,您可以在地图中使用[1: "value1",2: "value2",3: "value3"]
,并且下一次迭代您的地图可能是[1: "value1",2: "value2",3: "value3", 4: "value4"]
。主要问题是我认为MultiMap并不能确保元素入队的顺序(参见this post),因此你可以在迭代过程中跳过一个元素(它可以让你决定是否&# #39; s危险与否)
如果你真的需要停止每个put操作,你确实可以使用@Xaerxess方法同步 processData()中的地图。你提到的另一种可能性就是制作一些defensive copying,基本上迭代你的MultiMap快照,首先你会这样做:
public Multimap<String, Long> getClientidToTimestampHolder(){
return ImmutableSetMultimap.copyOf(clientidToTimestampHolder);
}
迭代将在这个快照上完成:
public void processData() {
Multimap<String, Long> tmpClientToTimestampHolder = getClientidToTimestampHolder();
for (Entry<String, Collection<Long>> entry : tmpClientToTimestampHolder.asMap().entrySet()) {
String clientid = entry.getKey();
Collection<Long> timestamps = entry.getValue();
for (long timestamp : timestamps) {
boolean isUpdated = isUpdatedClient(clientid, timestamp);
if (!isUpdated) {
updateClient(String.valueOf(clientid));
}
}
}
}
看到你对删除的评论,你会想要做一个同步的块来做atomically:
synchronized (clientidToTimestampHolder){
clientidToTimestampHolder.remove(key, value);//fill key,value, or use removAll(key)
}
为什么需要同步?因为如果您想在时间t获得精确的映射,那么您需要阻止其他线程向其添加元素。这是通过Java中的locking来完成的,因此只要一个线程(这里是你的后台线程)在地图上获得锁定,当你从中读取时,没有其他线程能够访问多重映射。