如何将条目从不同的线程填充到地图中,然后从单个后台线程填充地图并发送?

时间:2017-03-15 04:07:19

标签: java multithreading thread-safety guava multimap

我有一个下面的类,我有一个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();
      }
    }
  }

2 个答案:

答案 0 :(得分:2)

使用Multimaps.synchronized(List)Multimap包装器对多地图进行线程安全引用(ArrayListMultimapListMultimap,即将值存储在列表中):

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 #135this 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));
    }
  }
}

您可以(应该?)为您的数据创建自己的值类,以存储Stringlong字段,并使用它而不是通用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来完成的,因此只要一个线程(这里是你的后台线程)在地图上获得锁定,当你从中读取时,没有其他线程能够访问多重映射。