在遍历Map的entrySet时修改Map

3
在map接口的entrySet()方法的java文档中,我找到了这个声明,但我真的不理解它的意思。
集合由map支持,因此对map的更改会反映在集合中,反之亦然。如果在迭代集合时修改了map,则迭代的结果是未定义的。这里的“未定义”是什么意思?
为了更好地说明问题,这是我的情况。
我有一个基于Spring和Hibernate的Web应用程序。
我们的团队实现了自定义缓存类CachedIntegrationClients。
我们使用RabbitMQ作为消息服务器。
我们通过使用先前的缓存类缓存客户端,而不是每次想要向服务器发送消息时获取客户端。
问题是消息被发送到消息服务器两次。
查看日志,我们发现获取缓存客户端的方法返回客户端两次,尽管这在理论上是不可能的,因为我们将客户端存储在一个map中,而map不允许重复的键。
经过一些代码调试,我发现迭代缓存客户端的方法从缓存客户端map中获取客户端的集合。
因此,我怀疑在迭代此集合时,另一个客户端发出请求,并且此客户端可能未被缓存,因此它修改了map。
无论如何,这是CachedIntegrationClients类:
Map<String, IntegrationClient> cachedIntegrationClients = null;

@Override
public void setBaseDAO(BaseDao baseDao) {
    super.setBaseDAO(integrationDao);
}

@Override
public void refreshCache() {
    cachedIntegrationClients = null;
}

synchronized private void putOneIntegrationClientOnCache(IntegrationClient integrationClient){
    fillCachedIntegrationClients(); // only fill cache if it is null , it will never refill cache
    if (! cachedIntegrationClients.containsValue(integrationClient)) {
        cachedIntegrationClients.put(integrationClient.getClientSlug(),integrationClient);
    }
}

/**
 * only fill cache if it is null , it will never refill cache
 */
private void fillCachedIntegrationClients() {
    if (cachedIntegrationClients != null) {
        return ;
    }
    log.debug("filling cache of cachedClients");
    cachedIntegrationClients = new HashMap<String, IntegrationClient>(); // initialize cache Map
    List<IntegrationClient> allCachedIntegrationClients= integrationDao.getAllIntegrationClients();
    if (allCachedIntegrationClients != null) {
        for (IntegrationClient integrationClient : allCachedIntegrationClients) {
            integrationService
                    .injectCssFileForIntegrationClient(integrationClient);
            fetchClientServiceRelations(integrationClient
                    .getIntegrationClientServiceList());
        }
        for (IntegrationClient integrationClient : allCachedIntegrationClients) {
            putOneIntegrationClientOnCache(integrationClient);
        }
    }
}

/**
 * fetch all client service
 * @param integrationClientServiceList
 */
private void fetchClientServiceRelations(
        List<IntegrationClientService> integrationClientServiceList) {
    for (IntegrationClientService integrationClientService : integrationClientServiceList) {
        fetchClientServiceRelations(integrationClientService);
    }
}

private void fetchClientServiceRelations(IntegrationClientService clientService) {
    for (Exchange exchange : clientService.getExchangeList()) {
        exchange.getId();
    }
    for (Company company : clientService.getCompanyList()) {
        company.getId();
    }

}
/**
 * Get a client given its slug.
 * 
 * If the client was not found, an exception will be thrown.
 * 
 * @throws ClientNotFoundIntegrationException
 * @return IntegrationClient
 */
@Override
public IntegrationClient getIntegrationClient(String clientSlug) throws ClientNotFoundIntegrationException {
    if (cachedIntegrationClients == null) {
        fillCachedIntegrationClients();
    }

    if (!cachedIntegrationClients.containsKey(clientSlug)) {
        IntegrationClient integrationClient = integrationDao.getIntegrationClient(clientSlug);
        if (integrationClient != null) {
            this.fetchClientServiceRelations(integrationClient.getIntegrationClientServiceList());
            integrationService.injectCssFileForIntegrationClient(integrationClient);
            cachedIntegrationClients.put(clientSlug, integrationClient);
        }
    }

    IntegrationClient client = cachedIntegrationClients.get(clientSlug);
    if (client == null) {
        throw ClientNotFoundIntegrationException.forClientSlug(clientSlug);
    }
    return client;
}

public void setIntegrationDao(IntegrationDao integrationDao) {
    this.integrationDao = integrationDao;
}

public IntegrationDao getIntegrationDao() {
    return integrationDao;
}

public Map<String, IntegrationClient> getCachedIntegrationClients() {
    if (cachedIntegrationClients == null) {
        fillCachedIntegrationClients();
    }
    return cachedIntegrationClients;
}

public IntegrationService getIntegrationService() {
    return integrationService;
}

public void setIntegrationService(IntegrationService integrationService) {
    this.integrationService = integrationService;
}

以下是迭代集合的方法:

}

并且这里是迭代集合的方法:

public List<IntegrationClientService> getIntegrationClientServicesForService(IntegrationServiceModel service) {
        List<IntegrationClientService> integrationClientServices = new ArrayList<IntegrationClientService>();
        for (Entry<String, IntegrationClient> entry : cachedIntegrationClientService.getCachedIntegrationClients().entrySet()) {
            IntegrationClientService integrationClientService = getIntegrationClientService(entry.getValue(), service);
            if (integrationClientService != null) {
                integrationClientServices.add(integrationClientService);
            }
        }
        return integrationClientServices;
    }

这里是调用上一个方法的方法

List<IntegrationClientService> clients = integrationService.getIntegrationClientServicesForService(service);
    System.out.println(clients.size());
    if (clients.size() > 0) {
        log.info("Inbound service message [" + messageType.getKey() + "] to be sent to " + clients.size()
                + " registered clients: [" + StringUtils.arrayToDelimitedString(clients.toArray(), ", ") + "]");

        for (IntegrationClientService integrationClientService : clients) {
            Message<T> message = integrationMessageBuilder.build(messageType, payload, integrationClientService);
            try {
                channel.send(message);
            } catch (RuntimeException e) {
                messagingIntegrationService.handleException(e, messageType, integrationClientService, payload);
            }
        }
    } else {
        log.info("Inbound service message [" + messageType.getKey() + "] but no registered clients, not taking any further action.");
    }

以下是服务器上出现的日志记录:

BaseIntegrationGateway.createAndSendToSubscribers(65) | Inbound service message [news.create] to be sent to 3 registered clients: [Id=126, Service=IntegrationService.MESSAGE_NEWS, Client=MDC, Id=125, Service=IntegrationService.MESSAGE_NEWS, Client=CNBC, Id=125, Service=IntegrationService.MESSAGE_NEWS, Client=CNBC]
2个回答

4

Undefined 表示没有任何特定行为的要求,实现可以自由地开始第三次世界大战,通过上手法重新悬挂所有卫生纸,玷污您的祖母等。

唯一允许具有指定行为的修改是通过迭代器进行的。


2

你看过java.concurrent.ConcurrentHashMap吗?

编辑:我再次查看了你的代码,发现以下内容有些奇怪:

在fillCachedIntegrationClients()函数中,你有以下循环:

for (IntegrationClient integrationClient : allCachedIntegrationClients) {
        putOneIntegrationClientOnCache(integrationClient);
    }

但是putOneIntegrationClientOnCache方法本身直接调用fillCachedIntegrationClients();

synchronized private void putOneIntegrationClientOnCache(IntegrationClient integrationClient){
fillCachedIntegrationClients(); // only fill cache if it is null , it will never refill cache
...

有些地方一定出错了。你调用了fillCachedIntegrationClients()两次。实际上,如果我没有弄错的话,这应该是一个无限循环,因为一个方法调用另一个方法,反之亦然。在初始化期间,!= null条件从未满足。当然,你以一种不确定的方式修改和迭代,所以也许这可以避免无限循环。


1
谢谢BernardMarx的提示。我只想确保问题在并发性方面。请查看下一个答案并告诉我,地图是否可能返回两次条目。 - Fanooos
@Fanooos:它是否总是恰好两倍?还是根据测试用例而变化? - BernardMarx
关于你提到的代码,我注意到并进行了更改。关于地图中的重复项,实际上我无法在我的机器或测试服务器上重现这个问题,但它发生在生产服务器上,所以我编辑了上面的问题,并添加了出现的日志和另一种方法。 - Fanooos
在上面的日志中,您会发现该服务将向3个订阅者发送消息,并且存在一个订阅者重复。我还检查了数据库以查看此客户端是否在服务中订阅了两次,但没有。 - Fanooos
请问能否解释一下为什么在初始化期间,"!= null"条件从未被满足?如果map为空,则在第一次调用fill方法后,它将被实例化,在第二次调用时它不为空。这就是我理解的,除非我漏掉了什么。 - Fanooos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接