简体   繁体   English

由于 Java 9 HashMap.computeIfAbsent() 在尝试记住递归函数结果时抛出 ConcurrentModificationException

[英]Since Java 9 HashMap.computeIfAbsent() throws ConcurrentModificationException on attempt to memoize recursive function results

Today I learned from some JS course what memoization is and tried to implement it in Java.今天我从一些 JS 课程中了解到 memoization 是什么,并尝试在 Java 中实现它。 I had a simple recursive function to evaluate n-th Fibonacci number:我有一个简单的递归函数来计算第 n 个斐波那契数:

long fib(long n) {
    if (n < 2) {
        return n;
    }

    return fib(n - 1) + fib(n - 2);
}

Then I decided to use HashMap in order to cache results of recursive method:然后我决定使用 HashMap 来缓存递归方法的结果:

private static <I, O> Function<I, O> memoize(Function<I, O> fn) {
    final Map<I, O> cache = new HashMap<>();
    return in -> {
        if (cache.get(in) != null) {
            return cache.get(in);
        } else {
            O result = fn.apply(in);
            cache.put(in, result);
            return result;
        }
    };
}

This worked as I expected and it allowed me to memoize fib() like this memoize(this::fib)这按我的预期工作,它允许我像这样memoize(this::fib)记住fib() memoize(this::fib)

Then I googled the subject of memoization in Java and found the question: Java memoization method where computeIfAbsent was proposed which is much shorter than my conditional expression.然后我用computeIfAbsent搜索了 Java 中的记忆化主题,发现了一个问题: Java 记忆化方法,其中提出了computeIfAbsent这比我的条件表达式短得多。

So my final code which I expected to work was:所以我希望工作的最终代码是:

public class FibMemo {
    private final Function<Long, Long> memoizedFib = memoize(this::slowFib);

    public long fib(long n) {
        return memoizedFib.apply(n);
    }

    long slowFib(long n) {
        if (n < 2) {
            return n;
        }

        return fib(n - 1) + fib(n - 2);
    }

    private static <I, O> Function<I, O> memoize(Function<I, O> fn) {
        final Map<I, O> cache = new HashMap<>();
        return in -> cache.computeIfAbsent(in, fn);
    }

    public static void main(String[] args) {
        new FibMemo().fib(50);
    }
}

Since I used Java 11, I got:由于我使用了 Java 11,我得到了:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.HashMap.computeIfAbsent(HashMap.java:1134)
    at algocasts.matrix.fibonacci.FibMemo.lambda$memoize$0(FibMemo.java:24)
    at algocasts.matrix.fibonacci.FibMemo.fib(FibMemo.java:11)
    at algocasts.matrix.fibonacci.FibMemo.slowFib(FibMemo.java:19)
    at java.base/java.util.HashMap.computeIfAbsent(HashMap.java:1133)
    at algocasts.matrix.fibonacci.FibMemo.lambda$memoize$0(FibMemo.java:24)

The problem quickly brought me to the following question which is very similar: Recursive ConcurrentHashMap.computeIfAbsent() call never terminates.这个问题很快让我想到了以下非常相似的问题: Recursive ConcurrentHashMap.computeIfAbsent() call never terminates。 Bug or "feature"? 错误还是“功能”?

except Lukas used ConcurrentHashMap and got never ending loop.除了 Lukas 使用 ConcurrentHashMap 并获得了永无止境的循环。 In the article related to the question Lukas advised:在与卢卡斯建议的问题相关的文章中:

The simplest use-site solution for this concrete problem would be to not use a ConcurrentHashMap, but just a HashMap instead:对于这个具体问题,最简单的使用站点解决方案是不使用 ConcurrentHashMap,而只使用 HashMap:

static Map<Integer, Integer> cache = new HashMap<>();

That's exactly what I was trying to do, but did not succeed with Java 11. I found out empirically that HashMap throws ConcurrentModificationException since Java 9 (thanks SDKMAN):这正是我想要做的,但在 Java 11 中没有成功。我凭经验发现 HashMap 从 Java 9 开始抛出 ConcurrentModificationException(感谢 SDKMAN):

sdk use java 9.0.7-zulu && javac Test.java && java Test # throws ConcurrentModificationException
sdk use java 8.0.202-zulufx && javac Test.java && java Test # works as expected

Here are the summary of my attempts:以下是我的尝试总结:

  • Java 8 && ConcurrentHashMap -> works if input < 13, otherwise endless loop Java 8 && ConcurrentHashMap -> 如果输入 < 13,则工作,否则无限循环
  • Java 9 && ConcurrentHashMap -> works if input < 13, hangs if input = 14, throws IllegalStateException: Recursive update if input is 50 Java 9 && ConcurrentHashMap -> 如果输入 < 13 有效,如果输入 = 14 则挂起,如果输入为 50,则抛出IllegalStateException: Recursive update
  • Java 8 && HashMap -> Works! Java 8 && HashMap -> 有效!
  • Java 9 && HashMap -> Throws ConcurrentModificationException after input >= 3 Java 9 && HashMap -> 输入后抛出ConcurrentModificationException >= 3

I would like to know why ConcurrentModificationException gets thrown on attempt to use HashMap as a cache for recursive function?我想知道为什么在尝试使用 HashMap 作为递归函数的缓存时抛出 ConcurrentModificationException? Was it a bug in Java 8 allowing me to do so or is it another in Java > 9 which throws ConcurrentModificationException?它是 Java 8 中允许我这样做的错误还是 Java > 9 中的另一个抛出 ConcurrentModificationException 的错误?

ConcurrentModificationException is thrown because slowFib is modifying multiple keys and values.抛出ConcurrentModificationException是因为slowFib正在修改多个键和值。 If you look at Java 9 HashMap.computeIfAbsent() code you will find that the exception is thrown here:如果您查看Java 9 HashMap.computeIfAbsent()代码,您会发现这里抛出了异常:

int mc = modCount;
V v = mappingFunction.apply(key);
if (mc != modCount) { throw new ConcurrentModificationException(); }

Each invocation of slowFib attempts to modify values mapped to keys n-1 and n-2 .每次调用slowFib尝试修改映射到键n-1n-2

The modCount check is not performed in Java 8 HashMap.computeIfAbsent() code.在 Java 8 HashMap.computeIfAbsent()代码中不执行modCount检查。 This is a bug in Java 8, your approach doesn't work in all cases as per JDK-8071667 HashMap.computeIfAbsent() adds entry that HashMap.get() does not find which added the modCount check in Java 9:这是 Java 8 中的一个错误,根据JDK-8071667 HashMap.computeIfAbsent() 添加了 HashMap.get() 找不到的条目,在 Java 9 中添加了modCount检查,因此您的方法并不适用于所有情况:

If the function supplied to computeIfAbsent adds items to the same HashTable on which the function is called from and the internal table is enlarged because of this, the new entry will be added to the wrong place in the Map's internal table making it inaccessible.如果提供给 computeIfAbsent 的函数将项添加到调用该函数的同一个 HashTable 并且因此扩大了内部表,则新条目将被添加到 Map 内部表中的错误位置,使其无法访问。

You can roll your own:你可以推出自己的:

public static <K, V> V computeIfAbsent(
    Map<K, V> cache, 
    K key,
    Function<? super K, ? extends V> function
) {
    V result = cache.get(key);

    if (result == null) {
        result = function.apply(key);
        cache.put(key, result);
    }

    return result;
}

This approach assumes you don't have null values.这种方法假设您没有null值。 If you do, just change the logic to use Map.containsKey()如果这样做,只需更改逻辑以使用Map.containsKey()

This access works around the problem by making the cache value retrieval and putting of calculated values non-atomic again, which Map::computeIfAbsent tries to avoid.这种访问通过使缓存值检索和计算值再次非原子化来解决这个问题, Map::computeIfAbsent试图避免这种情况。 Ie the usual race condition problems re-occur.即通常的竞争条件问题再次发生。 But that might be fine in your case, of course.但是,当然,这对您的情况可能没问题。

What about this one?这个如何? Not sure about performance though...虽然不确定性能...

public class Fibonacci {

  private static final Map<Integer, Integer> memoized = new ConcurrentSkipListMap<>(Map.of(1,1,2,1));

  public static int fib(int n) {
    return memoized.computeIfAbsent(n, x -> fib(x - 2) + fib(x - 1));
  }

  public static void main(String[] args) {
    System.out.println(fib(12));
  }

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM