83 votes

Pourquoi ne courant parallèle avec lambda dans l'initialiseur statique provoquer un blocage?

Je suis tombé sur une drôle de situation où l'utilisation d'un courant parallèle avec un lambda dans un initialiseur statique prend apparemment pour toujours sans l'utilisation de l'UC. 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 minimum reproduction de cas de test pour ce comportement. Si J':

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

le code instantanément complète. Quelqu'un peut expliquer ce comportement? Est-ce un bug ou est-ce destiné?

Je suis en utilisant la version OpenJDK 1.8.0_66-interne.

71voto

Tunaki Points 2663

J'ai trouvé un rapport de bogue de très similaire (JDK-8143380) qui a été fermé parce que "Pas un Problème" par Stuart Marques:

C'est une classe d'initialisation de l'impasse. Le programme de test du thread principal exécute la classe d'initialiseur statique, qui définit l'initialisation en cours de drapeau pour la classe; ce drapeau reste ensemble jusqu'à ce que l'initialiseur statique est terminée. L'initialiseur statique exécute un courant parallèle, ce qui provoque des lambda expressions à évaluer dans d'autres threads. Ces blocs de threads en attente pour la classe pour terminer l'initialisation. Cependant, le thread principal est bloqué en attente pour le montage en parallèle des tâches à remplir, résultant dans l'impasse.

Le programme de test doit être modifié pour déplacer le courant parallèle de la logique à l'extérieur de la classe d'initialiseur statique. La clôture n'est Pas une Question.


J'ai été en mesure de trouver un autre rapport de bug (JDK-8136753), aussi fermés que "Pas un Problème" par Stuart Marques:

C'est un blocage qui se produit parce que le Fruit enum initialiseur statique, c'est l'interaction mal avec initialisation de classe.

Voir la Java Language Specification, la section 12.4.2 pour des détails sur l'initialisation de classe.

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

Brièvement, ce qui se passe est comme suit.

  1. Le thread principal des références de la des Fruits de la classe et commence le processus d'initialisation. Ceci définit l'initialisation en cours de drapeau et exécute l'initialiseur statique sur le thread principal.
  2. L'initialiseur statique exécute un code dans un autre thread, et attend qu'elle se termine. Cet exemple utilise les flux parallèles, mais cela n'a rien à voir avec les flux en soi. L'exécution de code dans un autre thread, par tous les moyens, et en attendant que le code à la fin, aura le même effet.
  3. Le code dans l'autre thread références le Fruit de la classe, qui vérifie l'initialisation en cours de drapeau. Cela provoque l'autre thread pour bloquer jusqu'à ce que la case est décochée. (Voir l'étape 2 de JLS 12.4.2.)
  4. Le thread principal est bloqué en attente pour l'autre thread de résilier, de sorte que l'initialiseur statique ne se termine jamais. Depuis l'initialisation en cours indicateur n'est pas libéré jusqu'à ce que après l'initialiseur statique est terminée, les fils sont dans l'impasse.

Pour éviter ce problème, assurez-vous qu'une classe statique de l'initialisation est terminée rapidement, sans causer d'autres threads pour exécuter du code qui a besoin de cette classe à avoir terminé l'initialisation.

La clôture n'est Pas une Question.


Notez que FindBugs a un problème ouvert pour l'ajout d'un avertissement pour cette situation.

16voto

hege_hegedus Points 569

Pour ceux qui se demandent où sont les autres threads du référencement de l' Deadlock de la classe elle-même, Java lambdas se comporter comme vous avez écrit ceci:

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

Régulièrement anonyme classes 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) {}
}

13voto

AdamSkywalker Points 4028

Il est une excellente explication de ce problème par Andrei Pangin, daté par 07 Avril 2015. Il est disponible ici, mais il est écrit en russe (je suggère d'examiner des exemples de code, de toute façon - ils à l'international). Le problème général est un verrou de classe pendant l'initialisation.

Voici quelques citations de l'article:


Selon JLS, chaque classe a un unique initialisation de verrouillage qui est capturé lors de l'initialisation. Lorsque d'autres thread tente d'accéder à cette classe lors de l'initialisation, il sera bloqué sur le verrou jusqu'à ce que l'initialisation est terminée. Lorsque les classes sont initialisés simultanément, il est possible d'obtenir une impasse.

J'ai écris un programme qui calcule la somme des entiers, que devrait-il imprimé?

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, enlevez parallel() ou remplacer lambda Integer::sum appelez - ce qui va changer?

Ici, nous voyons de blocage de nouveau [il y a quelques exemples de blocages dans la classe des initialiseurs précédemment dans l'article]. En raison de l' parallel() flux opérations à exécuter dans un thread séparé de la piscine. Ces threads tentent d'exécuter le corps de lambda, ce qui est écrit dans le bytecode en tant que private static méthode de StreamSum classe. Mais cette méthode ne peut pas être exécuté avant la fin de la classe de l'initialiseur statique, qui attend les résultats des cours d'achèvement.

Ce qui est plus mindblowing: ce code fonctionne différemment dans les différents environnements. Il fonctionne correctement sur un seul PROCESSEUR de la machine et sera plus susceptible de se bloquer sur un multi CPU de la machine. Cette différence provient du fait que le Fork-Join piscine de mise en œuvre. Vous pouvez le vérifier vous-même en changeant le paramètre -Djava.util.concurrent.ForkJoinPool.common.parallelism=N

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X