4

我有一个服务器类和一个定时器,它应该清除死客户端(崩溃的客户端)。我按照下面的示例在 Timer 遍历用户时锁定了集合,但我仍然得到这个异常(在我使连接的客户端崩溃之后)。

http://www.javaperformancetuning.com/articles/fastfail2.shtml

List<User> users;
List<User> connectedUsers;
ConcurrentMap<User, IClient> clients;

...

users = Collections.synchronizedList(new ArrayList<User>());
connectedUsers = new ArrayList<User>();
clients = new ConcurrentHashMap<User, IClient>();
timer = new Timer();
timer.schedule(new ClearDeadClients(), 5000, 5000);

...

class ClearDeadClients extends TimerTask {
    public void run() {
        synchronized (users) {
            Iterator<User> it = users.iterator();
            while (it.hasNext()) {
                User user = it.next(); // Throws exception
                if (!connectedUsers.contains(user)) {
                    users.remove(user);
                    clients.remove(user);
                }
            }
        }       

        connectedUsers.clear();
    }
}
4

2 回答 2

11

您需要从迭代器中删除而不是从集合中删除。它看起来像这样:

Iterator<User> it = users.iterator();
while (it.hasNext()) {
    User user = it.next(); 
    if (!connectedUsers.contains(user)) {
         it.remove();
         clients.remove(user);
     }
}
于 2011-02-28T17:12:11.460 回答
9

您不能在迭代集合时修改它 - 不幸的是,您在此处使用 执行此操作,users结果是 ConcurrentModificationException。来自 ArrayList 自己的javadocs

此类的iteratorlistIterator方法返回的迭代器是快速失败的:如果在创建迭代器后的任何时候列表在结构上被修改,除了通过迭代器自己的removeadd方法之外的任何方式,迭代器将抛出一个ConcurrentModificationException. 因此,面对并发修改,迭代器快速而干净地失败,而不是在未来不确定的时间冒任意的、非确定性的行为。

要解决这种特殊情况,您可以改用迭代器自己的remove()方法,方法是替换以下行:

users.remove(user);

it.remove();

后一个操作从集合中删除迭代器返回的最后一个元素。(这种用法避免了异常,因为迭代器知道更改并能够确保其安全;通过外部修改,迭代器无法知道其遍历的状态是否仍然一致,因此会快速失败)。

在某些情况下,这种立即删除可能不可行,在这种情况下,有三种可供选择的通用方法:

  1. 获取集合的副本(users在这种情况下),遍历副本并从原始集合中删除元素。
  2. 在迭代期间,构建一组要删除的元素,然后在迭代完成后执行批量删除。
  3. 使用List可以处理并发修改的实现,例如CopyOnWriteArrayList

这是一个非常常见的问题 - 另请参阅(例如)列表中的循环以及删除问题以获取其他答案。

于 2011-02-28T17:11:50.090 回答