在 ConcurrentHashMap 中以原子方式 searchKeys() 和 put()

Ale*_*chi 4 java concurrency concurrenthashmap java.util.concurrent

我正在用 java 开发一个 web 服务器,除其他外,它应该在几个用户之间实现一个挑战服务。
每个用户一次只能参加一项挑战。
实际上,我将“挑战”对象存储在 a 中ConcurrentHashMap<String, Challenge>,我使用的String是两个玩家用户名的并集作为映射键。
例如,如果两个玩家的用户名是“Mickey”和“Goofy”,那么Challenge里面的对象的键ConcurrentHashMap就是字符串:

Mickey:Goofy

在 中记录两个用户之间的新挑战时ConcurrentHashMap,我必须在实际将挑战放入 Map 之前检查他们是否已经参与其他挑战,换句话说,我必须检查 Map 中是否存储了一个密钥包含想要开始新挑战的玩家的两个用户名之一。

例如,给定ConcurrentHashMap<String, Challenge>对用户Mickey和的填充和挑战请求Goofy,我想以原子方式知道并且不锁定整个地图,如果他们中的一个(或最终两个)已经参与了地图中的其他注册挑战如果没有,则将新挑战放入地图中。
我希望已经足够清楚了。

你们有什么建议吗?

提前致谢。

Hol*_*ger 6

对于复合键,使用字符串连接是一个糟糕的选择。字符串连接是一项开销很大的操作,它不能保证唯一性,因为当其中一个字符串包含您选择的分隔符时,键就会变得不明确。

当然,您可以禁止用户名中的特定字符,但这会增加您必须检查的额外要求,而拥有两个引用的专用键对象更简单、更有效。你甚至可以使用两个元素List<String>作为附加特殊的密钥类型,因为它具有非常有用hashCodeequals实现。

但是由于无论如何您都想对复合键的两个部分执行查找,因此您首先不应该使用复合键。只需将两个用户名与同一个Challenge对象相关联。这仍然无法在单个原子操作中完成,但它不需要:

final ConcurrentHashMap<String, Challenge> challenges = new ConcurrentHashMap<>();

Challenge startNewChallenge(String user1, String user2) {
    if(user1.equals(user2))
        throw new IllegalArgumentException("same user");

    Challenge c = new Challenge();

    if(challenges.putIfAbsent(user1, c) != null)
        throw new IllegalStateException(user1+" has an ongoing challenge");

    if(challenges.putIfAbsent(user2, c) != null) {
        challenges.remove(user1, c);
        throw new IllegalStateException(user2+" has an ongoing challenge");
    }

    return c;
}
Run Code Online (Sandbox Code Playgroud)

此代码永远不会覆盖现有值。如果两者putIfAbsent都成功,则两个用户肯定都没有正在进行的挑战,并且现在都与同一个新挑战相关联。

当第一个putIfAbsent成功但第二个失败时,我们必须删除第一个关联。remove(user1, c)只有当用户仍然与我们的新挑战相关联时才会删除它。当地图上的所有操作都遵循永不覆盖现有条目的原则时(除非满足所有先决条件),这不是必需的,平原remove(user1)也可以。但是在这里使用安全变体并没有什么坏处。

非原子性的唯一问题是,由于临时添加的第一个用户,涉及同一用户的两次重叠尝试可能都失败,而实际上其中一个可能会成功。我不认为这是一个重大问题;用户根本不应该尝试同时加入两个挑战。

  • 重要的一点是,如果一个线程查询挑战的密钥并处理挑战,而另一个线程将其从映射中删除,会发生什么。如果这种并发性带来了问题,您需要额外的机制来锁定特定的质询对象或使该质询对象失效。除此之外,删除本身并不是一个大问题 1) 检查两个用户是否与同一个挑战对象关联 2) 使用答案中显示的“remove(key, ExpectedValue)”方法,仅在仍然映射到的情况下安全删除两个密钥已经检查过的挑战。 (2认同)
  • @Tarun 想象一下在最初为空的地图上对 `startNewChallenge("foo", "bar")` 和 `startNewChallenge("bar", "foo")` 进行两次并发尝试。原则上,可以接受一个而只拒绝另一个,如果他们不同时到达地图,这正是会发生的情况。但两者可能同时运行,第一个“putIfAbsent”成功执行,然后第二个失败,因为第二个密钥已经注册。所以这两种尝试都被拒绝了。 (2认同)
  • @Tarun 还没有。也许,有一天我会开始一个…… (2认同)