Рекурсивний виклик ConcurrentHashMap.computeIfAbsent () ніколи не припиняється. Помилка чи “функція”?


76

Деякий час тому я писав у блозі про функціональний спосіб Java 8 для обчислення чисел Фібоначчі рекурсивно , з ConcurrentHashMapкешем та новим, корисним computeIfAbsent()методом:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
    static Map<Integer, Integer> cache = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        System.out.println(
            "f(" + 8 + ") = " + fibonacci(8));
    }

    static int fibonacci(int i) {
        if (i == 0)
            return i;

        if (i == 1)
            return 1;

        return cache.computeIfAbsent(i, (key) -> {
            System.out.println(
                "Slow calculation of " + key);

            return fibonacci(i - 2) + fibonacci(i - 1);
        });
    }
}

Я вибрав, ConcurrentHashMapбо думав зробити цей приклад ще більш витонченим, запровадивши паралелізм (чого зрештою не зробив).

Тепер давайте збільшимо число від 8до 25і спостерігатимемо, що відбувається:

        System.out.println(
            "f(" + 25 + ") = " + fibonacci(25));

Програма ніколи не зупиняється. Усередині методу є цикл, який просто працює вічно:

for (Node<K,V>[] tab = table;;) {
    // ...
}

Я використовую:

C:\Users\Lukas>java -version
java version "1.8.0_40-ea"
Java(TM) SE Runtime Environment (build 1.8.0_40-ea-b23)
Java HotSpot(TM) 64-Bit Server VM (build 25.40-b25, mixed mode)

Матіас, читач цієї публікації в блозі, також підтвердив проблему (він насправді її знайшов) .

Це дивно. Я б очікував будь-якого з наступних двох:

  • Це працює
  • Це кидає a ConcurrentModificationException

Але просто ніколи не зупиняючись? Це здається небезпечним. Це помилка? Або я неправильно зрозумів якийсь контракт?

Відповіді:


58

Це виправлено у JDK-8062841 .

У пропозиції 2011 року я виявив цю проблему під час перегляду коду. JavaDoc було оновлено та додано тимчасове виправлення. Його було видалено під час подальшого перезапису через проблеми з продуктивністю.

В обговоренні 2014 року ми дослідили шляхи кращого виявлення та невдачі. Зверніть увагу, що частина обговорення була перенесена в автономний режим на приватну електронну пошту для розгляду змін на низькому рівні. Незважаючи на те, що не кожна справа може бути висвітлена, загальні справи не будуть цікавими. Ці виправлення містяться у сховищі Дуга, але не перетворили його на випуск JDK.


5
Дуже цікаво, дякую за поділ! Мою доповідь також прийняли як JDK-8074374
Лукас Едер,

1
@Ben вибачте за офтоп, але я справді здивований тим, наскільки низький рейтинг SO у вас є, незважаючи на настільки значний внесок у базу знань щодо такої складної речі, як вільний від замків (і близький до блокування) паралельність.
Alex Salauyou

@SashaSalauyou Дякую. Я часто даю швидку відповідь у коментарях, а не витрачаю час на довшу, повну відповідь. Оцінки коментарів, однак, не підвищують репутацію. У мене, мабуть, менше, ніж середній інтерес до переслідування представників.
Бен Манес

@Ben не зовсім виправлений: бум
Скотт

@ScottMcKinney приємний улов! Схоже, до цього самого твердження можна було застосувати, transferі це був пропущений випадок. Чи можете ви надіслати електронною поштою, concurrency-interest@cs.oswego.eduщоб привернути увагу Дага?
Ben Manes

56

Це, звичайно, "особливість" . У ConcurrentHashMap.computeIfAbsent()Javadoc написано:

Якщо вказаний ключ ще не пов'язаний зі значенням, намагається обчислити його значення за допомогою заданої функції зіставлення та вводить його на цю карту, якщо не має значення null. Весь виклик методу виконується атомарно, тому функція застосовується не більше одного разу для ключа. Деякі спроби операцій оновлення на цій карті іншими потоками можуть бути заблоковані під час обчислення, тому обчислення повинно бути коротким і простим і не повинно намагатися оновити будь-які інші зіставлення цієї карти .

«Не винен» формулювання чіткий контракт, який мій алгоритм порушується, хоча і не за тими ж причини паралелізму.

Що все-таки цікаво - немає ConcurrentModificationException. Натомість програма просто ніколи не зупиняється - що, на мій погляд, все ще є досить небезпечною помилкою (тобто нескінченні цикли. Або: все, що може піти не так, робить ).

Примітка:

HashMap.computeIfAbsent()Або Map.computeIfAbsent()Javadoc не забороняють таке рекурсивне обчислення, яке, звичайно , смішно , як тип кешу Map<Integer, Integer>, а НЕ ConcurrentHashMap<Integer, Integer>. Дуже небезпечно для підтипів різко переосмислювати контракти Setсупертипу ( проти SortedSetпривітання). Таким чином, слід забороняти також у супертипах робити таку рекурсію.


3
Гарна знахідка. Я б запропонував повідомлення про помилку / RFE щодо JDK.
Аксель

4
Готово, давайте подивимось, чи прийнято це ... Я оновлю посиланням, якщо так.
Лукас Едер

3
Мені здається ймовірним, що цей тип рекурсивної модифікації інших зіставлення заборонений ConcurrentHashMapчерез іншу важливу частину його контракту: " Весь виклик методу виконується атомарно, тому функція застосовується щонайбільше один раз для ключа ". ймовірно, що ваша програма, порушуючи контракт "без рекурсивної модифікації", намагається отримати блокування, яке вже має, і робить глухий кут із собою, не працюючи в нескінченному циклі.
murgatroid99

3
З JavaDoc:IllegalStateException - if the computation detectably attempts a recursive update to this map that would otherwise never complete
Бен Манес

2
@LukasEder навіть з HashMapцим не в порядку. bugs.openjdk.java.net/browse/JDK-8172951
Piotr

4

Це дуже схоже на помилку. Тому що, якщо ви створите свій кеш ємністю 32, ваша програма буде працювати до 49. І цікаво, що параметр sizeCtl = 32 + (32 >>> 1) + 1) = 49! Може бути причина зміни розміру?


1
У мене немає часу копатись у вихідному коді, але я думаю, що ви маєте рацію. Ми досягаємо цього у виробництві, постійно. Як тільки ми збільшили CHMпочаткову потужність до дуже високого показника, це пішло. Поки ми не зможемо зробити рефакторинг нашого коду, ми будемо це робити ...
Євген
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.