web-dev-qa-db-fra.com

L'appel récursif ConcurrentHashMap.computeIfAbsent () ne se termine jamais. Bug ou "fonctionnalité"?

Il y a quelque temps, J'ai blogué sur un Java 8 façon fonctionnelle de calculer les nombres de fibonacci de manière récursive , avec un cache ConcurrentHashMap et le nouveau, utile computeIfAbsent() méthode:

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);
        });
    }
}

J'ai choisi ConcurrentHashMap parce que je pensais rendre cet exemple encore plus sophistiqué en introduisant le parallélisme (ce que je n'ai finalement pas fait).

Maintenant, augmentons le nombre de 8 à 25 et observez ce qui se passe:

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

Le programme ne s'arrête jamais. À l'intérieur de la méthode, il y a une boucle qui s'exécute pour toujours:

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

J'utilise:

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)

Matthias, un lecteur de ce billet de blog a également confirmé le problème (il l'a effectivement trouvé) .

C'est bizarre. Je m'attendais à l'un des deux suivants:

  • Ça marche
  • Il lance un ConcurrentModificationException

Mais ne vous arrêtez jamais? Cela semble dangereux. Est-ce un bug? Ou ai-je mal compris un contrat?

71
Lukas Eder

Ceci est corrigé dans JDK-8062841 .

Dans la proposition 2011 , j'ai identifié ce problème lors de la révision du code. Le JavaDoc a été mis à jour et un correctif temporaire a été ajouté. Il a été supprimé lors d'une nouvelle réécriture en raison de problèmes de performances.

Dans la discussion de 2014 , nous avons exploré des moyens de mieux détecter et échouer. Notez qu'une partie de la discussion a été mise hors ligne vers un e-mail privé pour prendre en compte les modifications de bas niveau. Bien que tous les cas ne puissent pas être couverts, les cas courants ne seront pas verrouillés. Ces correctifs sont dans le référentiel de Doug mais ne sont pas entrés dans une version JDK.

50
Ben Manes

C'est bien sûr un "fonctionnalité". La ConcurrentHashMap.computeIfAbsent() Javadoc lit:

Si la clé spécifiée n'est pas déjà associée à une valeur, tente de calculer sa valeur à l'aide de la fonction de mappage donnée et la saisit dans cette carte, sauf si elle est nulle. L'invocation de la méthode entière est effectuée de manière atomique, de sorte que la fonction est appliquée au plus une fois par clé. Certaines tentatives de mise à jour sur cette carte par d'autres threads peuvent être bloquées pendant le calcul, donc le calcul doit être court et simple, et ne doit pas tenter de mettre à jour d'autres mappages de cette carte.

Le libellé "ne doit pas" est un contrat clair, que mon algorithme a violé, mais pas pour les mêmes raisons de concurrence.

Ce qui est encore intéressant, c'est qu'il n'y a pas de ConcurrentModificationException. Au lieu de cela, le programme ne s'arrête jamais - ce qui est toujours un bug plutôt dangereux à mon avis (c'est-à-dire boucles infinies ou: tout ce qui peut mal tourner, le fait ).

Remarque:

Le Javadoc HashMap.computeIfAbsent() ou Map.computeIfAbsent() n'interdit pas un tel calcul récursif, ce qui est bien sûr ridicule comme le type du cache est Map<Integer, Integer>, ne pas ConcurrentHashMap<Integer, Integer>. Il est très dangereux pour les sous-types de redéfinir radicalement les contrats de super-type (Set contre SortedSet est le message d'accueil). Il devrait donc être également interdit dans les super types, d'effectuer une telle récursivité.

53
Lukas Eder

Ceci est très similaire au bug. Car, si vous créez votre cache de capacité 32, votre programme fonctionnera jusqu'à 49. Et c'est intéressant, ce paramètre sizeCtl = 32 + (32 >>> 1) + 1) = 49! Peut-être la raison du redimensionnement?