迭代并发集合时的线程安全性

时间:2010-10-16 19:09:14

标签: java multithreading synchronization concurrenthashmap

我正在编写一些客户端 - 服务器应用程序,我必须处理多个线程。我有一些服务器,每隔几秒发送一次活动包。这些服务器维护在ConcurrentHashMap中,其中包含EndPoints与最后一个alive-package到达相应服务器的时间。

现在我有一个线程,必须“整理”所有未在特定时间内发送alive-packets的服务器。

我想我不能那样做,可以吗?

for( IPEndPoint server : this.fileservers.keySet() )
{
    Long time = this.fileservers.get( server );

    //If server's time is updated here, I got a problem

    if( time > fileserverTimeout )
        this.fileservers.remove( server );
}

有没有一种方法可以绕过它而不需要锁定整个循环(我必须在其他线程中尊重)?

4 个答案:

答案 0 :(得分:3)

这里可能没有问题,具体取决于您在地图中存储的确切内容。您的代码看起来有点奇怪,因为您似乎保存了“服务器未处于活动状态的持续时间”。

我记录该数据的第一个想法是存储“服务器处于活动状态的最新时间戳”。然后你的代码看起来像这样:

package so3950354;

import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class ServerManager {

  private final ConcurrentMap<Server, Long> lastActive = new ConcurrentHashMap<Server, Long>();

  /** May be overridden by a special method for testing. */
  protected long now() {
    return System.currentTimeMillis();
  }

  public void markActive(Server server) {
    lastActive.put(server, Long.valueOf(now()));
  }

  public void removeInactive(long timeoutMillis) {
    final long now = now();

    Iterator<Map.Entry<Server, Long>> it = lastActive.entrySet().iterator();
    while (it.hasNext()) {
      final Map.Entry<Server, Long> entry = it.next();
      final long backThen = entry.getValue().longValue();
      /*
       * Even if some other code updates the timestamp of this server now,
       * the server had timed out at some point in time, so it may be
       * removed. It's bad luck, but impossible to avoid.
       */
      if (now - backThen >= timeoutMillis) {
        it.remove();
      }
    }
  }

  static class Server {

  }
}

如果你真的想避免在调用markActive期间没有代码曾经调用removeInactive,那么就无法显式锁定。你可能想要的是:

  • 允许同时调用markActive
  • markActive期间,不允许拨打removeInactive
  • removeInactive期间,不允许拨打markActive

这看起来像ReadWriteLock的典型情况,其中markActive是“阅读”操作,removeInactive是“写作”操作。

答案 1 :(得分:1)

我没有看到另一个线程如何在代码中更新服务器的时间。使用this.fileservers.get( server )从地图检索服务器的时间后,另一个线程无法更改其值,因为Long对象是不可变的。是的,另一个线程可以将该服务器的新Long对象放入映射中,但这不会影响该线程,因为它已经检索了服务器的时间。

因此,我认为你的代码没有任何问题。 ConcurrentHashMap中的迭代器弱一致,这意味着它们可以容忍并发修改,因此不存在抛出ConcurrentModificationException的风险。

答案 2 :(得分:0)

(请参阅Roland's answer,其中包含了这些想法并将其充实为更完整的示例,并提供了一些很好的额外见解。)

由于它是并发哈希映射,因此您可以执行以下操作。请注意,CHM的迭代器都实现了您想要的可选方法,包括remove()。请参阅CHM API docs,其中说明:

  

这个类及其视图和迭代器   实现所有可选方法   MapIterator接口。

此代码应该有效(我不知道CHM中Key的类型):

ConcurrentHashMap<K,Long> fileservers = ...;

for(Iterator<Map.Entry<K,Long>> fsIter = fileservers.entrySet().iterator(); fileservers.hasNext(); )
{
    Map.Entry<K,Long> thisEntry = fsIter.next();
    Long time = thisEntry.getValue();

    if( time > fileserverTimeout )
        fsIter.remove( server );
}

但请注意,其他地方可能存在竞争条件......你需要确保访问地图的其他代码可以应对这种自发删除 - 也就是说,无论你触摸fileservers.put()你是什么我需要一些涉及fileservers.putIfAbsent()的逻辑。与使用synchronized相比,此解决方案不太可能产生瓶颈,但还需要更多考虑。

你在哪里写道“如果服务器的时间在这里更新,我遇到了问题”正是putIfAbsent()进来的地方。如果条目不存在,要么你以前没见过,要么你刚刚放弃了它来自桌子。如果需要协调这两方面,那么您可能想要为该条目引入可锁定记录,并在该级别执行同步(即,在执行remove()时同步记录,而不是在整个表上)。然后事件的put()结束也可以在同一记录上同步,从而消除潜在的竞争。

答案 3 :(得分:-1)

首先使地图同步

this.fileservers = Collections.synchronizedMap(Map)

然后使用Singleton类中使用的策略

if( time > fileserverTimeout )
    {
        synchronized(this.fileservers)
        {
             if( time > fileserverTimeout )
                  this.fileservers.remove( server );
        }
    }

现在确保一旦进入synchronized块,就不会发生更新。这是因为一旦锁定在地图上,map(同步包装器)将无法自行提供线程锁定以进行更新,删除等。

检查时间两次确保仅在存在真正的删除情况时才使用同步