web-dev-qa-db-fra.com

Pourquoi le flux parallèle avec lambda dans l'initialiseur statique provoque-t-il un blocage?

Je suis tombé sur une situation étrange où l'utilisation d'un flux parallèle avec un lambda dans un initialiseur statique prend apparemment une éternité sans utilisation du processeur. Voici le code:

class Deadlock {
    static {
        IntStream.range(0, 10000).parallel().map(i -> i).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

Cela semble être un cas de test de reproduction minimum pour ce comportement. Si je:

  • mettre le bloc dans la méthode principale au lieu d'un initialiseur statique,
  • supprimer la parallélisation, ou
  • retirer la lambda,

le code se termine instantanément. Quelqu'un peut-il expliquer ce comportement? Est-ce un bug ou est-ce prévu?

J'utilise OpenJDK version 1.8.0_66-internal.

76
Solomonoff's Secret

J'ai trouvé un rapport de bogue d'un cas très similaire ( JDK-814338 ) qui a été fermé comme "Pas un problème" par Stuart Marks:

Il s'agit d'un blocage d'initialisation de classe. Le thread principal du programme de test exécute l'initialiseur statique de classe, qui définit l'indicateur d'initialisation en cours pour la classe; cet indicateur reste activé jusqu'à la fin de l'initialisation statique. L'initialiseur statique exécute un flux parallèle, ce qui entraîne l'évaluation des expressions lambda dans d'autres threads. Ces threads bloquent l'attente de la fin de l'initialisation de la classe. Cependant, le thread principal est bloqué en attendant la fin des tâches parallèles, ce qui entraîne un blocage.

Le programme de test doit être modifié pour déplacer la logique du flux parallèle en dehors de l'initialiseur statique de classe. Clore comme pas un problème.


J'ai pu trouver un autre rapport de bogue de cela ( JDK-813675 ), également fermé comme "Pas un problème" par Stuart Marks:

Il s'agit d'un blocage qui se produit car l'initialiseur statique de l'énumération Fruit interagit mal avec l'initialisation de la classe.

Voir la spécification du langage Java, section 12.4.2 pour plus de détails sur l'initialisation de classe.

http://docs.Oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2

En bref, ce qui se passe est le suivant.

  1. Le thread principal fait référence à la classe Fruit et démarre le processus d'initialisation. Cela définit l'indicateur d'initialisation en cours et exécute l'initialiseur statique sur le thread principal.
  2. L'initialiseur statique exécute du code dans un autre thread et attend qu'il se termine. Cet exemple utilise des flux parallèles, mais cela n'a rien à voir avec les flux en soi. Exécuter du code dans un autre thread par n'importe quel moyen et attendre la fin de ce code aura le même effet.
  3. Le code de l'autre thread fait référence à la classe Fruit, qui vérifie l'indicateur d'initialisation en cours. Cela provoque le blocage de l'autre thread jusqu'à ce que l'indicateur soit effacé. (Voir l'étape 2 de JLS 12.4.2.)
  4. Le thread principal est bloqué en attendant la fin de l'autre thread, donc l'initialiseur statique ne se termine jamais. Étant donné que l'indicateur d'initialisation en cours n'est effacé qu'après la fin de l'initialisation statique, les threads sont bloqués.

Pour éviter ce problème, assurez-vous que l'initialisation statique d'une classe se termine rapidement, sans que d'autres threads exécutent du code qui nécessite que cette classe ait terminé l'initialisation.

Clore comme pas un problème.


Notez que FindBugs a un problème ouvert pour ajouter un avertissement pour cette situation.

66
Tunaki

Pour ceux qui se demandent où sont les autres threads référençant la classe Deadlock elle-même, Java lambdas se comportent comme vous l'avez écrit:

public class Deadlock {
    public static int lambda1(int i) {
        return i;
    }
    static {
        IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
            @Override
            public int applyAsInt(int operand) {
                return lambda1(operand);
            }
        }).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}

Avec les classes anonymes régulières, il n'y a pas de blocage:

public class Deadlock {
    static {
        IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() {
            @Override
            public int applyAsInt(int operand) {
                return operand;
            }
        }).count();
        System.out.println("done");
    }
    public static void main(final String[] args) {}
}
16
Tamas Hegedus

Il y a une excellente explication de ce problème par Andrei Pangin , daté du 07 avril 2015. Il est disponible ici , mais il est écrit en russe (je suggère de revoir les exemples de code de toute façon - ils sont internationaux). Le problème général est un verrou pendant l'initialisation de la classe.

Voici quelques citations de l'article:


Selon JLS , chaque classe a un verrou d'initialisation unique qui est capturé pendant initialisation. Lorsqu'un autre thread essaie d'accéder à cette classe pendant l'initialisation, elle sera bloquée sur le verrou jusqu'à la fin de l'initialisation. Lorsque les classes sont initialisées simultanément, il est possible d'obtenir un blocage.

J'ai écrit un programme simple qui calcule la somme des entiers, que devrait-il imprimer?

public class StreamSum {
    static final int SUM = IntStream.range(0, 100).parallel().reduce((n, m) -> n + m).getAsInt();

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

Maintenant, supprimez parallel() ou remplacez lambda par Integer::sum Appel - qu'est-ce qui va changer?

Ici, nous voyons à nouveau l'impasse [il y avait quelques exemples de blocages dans les initialiseurs de classe précédemment dans l'article]. En raison de la parallel() les opérations de flux s'exécutent dans un pool de threads distinct. Ces threads essaient d'exécuter le corps lambda, qui est écrit en bytecode en tant que méthode private static Dans la classe StreamSum. Mais cette méthode ne peut pas être exécutée avant l'achèvement de l'initialiseur statique de classe, qui attend les résultats de l'achèvement du flux.

Quoi de plus époustouflant: ce code fonctionne différemment dans différents environnements. Il fonctionnera correctement sur une seule machine CPU et se bloquera très probablement sur une machine multi CPU. Cette différence provient de l'implémentation du pool Fork-Join. Vous pouvez le vérifier vous-même en modifiant le paramètre -Djava.util.concurrent.ForkJoinPool.common.parallelism=N

13
AdamSkywalker