如何使用 ConcurrentHashMap 执行线程安全的获取然后删除?

How can I perform a thread-safe get then remove with ConcurrentHashMap?

在一次采访中,我被要求检查以下代码是否按预期工作。

ConcurrentHashMap<Integer, Integer> chm = new ConcurrentHashMap<>();

if (chm.get(key) != null) {
    chm.get(key).doSomething();
    chm.remove(key);
}

根据 JavaDocs,get returns 最后完成更新操作的值。因此,如果线程 1 已经调用了 chm.remove(key) 并且如果线程 2 进入了 if 语句并且即将调用 get 方法,那么我们可能会得到一个异常。这是正确的吗?

如何使这个线程安全?

你是对的。如果这个 Map 可以被多个线程修改,那么第一次调用 chm.get(key) 可能会 return 一个非空值,而第二次调用 return [=13] =](由于另一个线程从 Map 中删除了密钥),因此 chm.get(key).doSomething() 将抛出 NullPointerException.

您可以通过使用局部变量来存储 chm.get(key):

的结果来使此代码线程安全
ConcurrentHashMap<Integer, Integer> chm = new ConcurrentHashMap<Integer, Integer>();
Integer value = chm.get(key);

if(value != null) {
    value.doSomething(); // P.S. Integer class doesn't have a doSomething() method
                         // but I guess this is just an example of calling some arbitrary 
                         // instance method
    chm.remove(key);
}

顺便说一句,即使 Map 不是 ConcurentHashMap 并且只有一个线程可以访问它,我仍然会使用局部变量,因为它比调用 [= =20=]方法两次。

编辑:

如下所述,此修复不会阻止 doSomething() 被不同线程多次调用相同的 key/value。这是否是所需的行为尚不清楚。

如果您希望防止 doSomething() 被同一个 key/value 的多个线程调用的可能性,您可以使用 chm.remove(key) 来删除键和获取值同样的步骤。

然而,这存在 doSomething() 对于某些 key/value 根本不会执行的风险,因为如果第一次调用 doSomething() 导致异常,则不会另一个线程对 doSomething() 的另一个调用,因为 key/value 对将不再在 Map 中。另一方面,如果仅在 doSomething() 成功执行后才从 Map 中删除 key/value 对,则可以保证 doSomething() 对所有 key/value 至少执行一次从 Map.

中删除的对

Map.remove(key) returns 已删除的值。这在很多情况下都是一个很好的技巧,包括你的情况:

Object value = chm.remove(key)
if(value != null)
{
     value.doSomething();
}

你不能安全地使用 get 然后 remove,因为如果两个线程同时调用你的方法,他们总是有风险调用 doSomething 两次或更多次,然后键被已删除。

如果先删除它,这是不可能的。上面的代码是线程安全的,也更简单。