从多个线程填充地图

Populating map from multiple threads

我有一个从多个线程填充的 ConcurrentHashMap,如下所示:

private static Map<ErrorData, Long> holder = new ConcurrentHashMap<ErrorData, Long>();

public static void addError(ErrorData error) {
    if (holder.keySet().contains(error)) {
        holder.put(error, holder.get(error) + 1);
    } else {
        holder.put(error, 1L);
    }
}

上面的代码中是否存在竞争条件的可能性并且它可以跳过更新?另外,如果 Guava AtomicLongMap 可以提供更好的性能,我该如何在这里使用它?

我在Java 7.

如果您使用的是 java 8,则可以利用新的 merge 方法:

holder.merge(error, 1L, Long::sum);

是的,存在竞争的可能性,因为您没有检查包含和放置原子。

您可以按如下方式使用 AtomicLongMap,它会自动执行此检查:

private static final AtomicLongMap<ErrorData> holder = AtomicLongMap.create();

public static void addError(ErrorData error) {
  holder.getAndIncrement(error);
}

如 javadoc 中所述:

[T]he typical mechanism for writing to this map is addAndGet(K, long), which adds a long to the value currently associated with K. If a key has not yet been associated with a value, its implicit value is zero.

All operations are atomic unless otherwise noted.

A 'vanilla' java 5+解法:

public static void addError(final ErrorData errorData) {
    Long previous = holder.putIfAbsent(errorData, 1L);
    // if the error data is already mapped to some value
    if (previous != null) {
        // try to replace the existing value till no update takes place in the meantime
        while (!map.replace(errorData, previous, previous + 1)) {
            previous = map.get(errorData);
        }
    }
}

在 Java 7 或更早的版本中,您需要使用比较和更新循环:

Long prevValue;
boolean done;
do {
  prevValue = holder.get(error);
  if (prevValue == null) {
    done = holder.putIfAbsent(error, 1L);
  } else {
    done = holder.replace(error, prevValue, newValue);
  }
} while (!done);

使用此代码,如果两个线程争用一个可能会重试其更新,但它们最终会获得正确的值。

考虑:

Thread1: holder.get(error) returns 1
Thread2: holder.get(error) returns 1
Thread1: holder.put(error, 1+1);
Thread2: holder.put(error, 1+1);

要解决此问题,您需要使用原子操作来更新地图。