Je suis tombé sur une situation étrange où l’utilisation d’un stream 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 Ssortingng[] args) {} }
Cela semble être un cas de test de reproduction minimum pour ce comportement. Si je:
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.
J’ai trouvé un rapport de bogue sur un cas très similaire ( JDK-8143380 ) qui a été clôturé par “Pas un problème” par Stuart Marks:
Ceci est un blocage de l’initialisation de la classe. Le thread principal du programme de test exécute l’initialiseur statique de la classe, qui définit l’indicateur d’initialisation en cours pour la classe; Cet indicateur rest défini jusqu’à ce que l’initialiseur statique se termine. L’initialiseur statique exécute un stream parallèle, ce qui entraîne l’évaluation des expressions lambda dans d’autres threads. Ces threads bloquent l’attente de l’initialisation de la classe. Toutefois, le thread principal est bloqué en attendant que les tâches parallèles se terminent, ce qui entraîne un blocage.
Le programme de test doit être modifié pour déplacer la logique de stream parallèle en dehors de l’initialiseur statique de classe. Fermer en tant que pas un problème.
J’ai été en mesure de trouver un autre rapport de bogue à ce sujet ( JDK-8136753 ), également fermé par “Pas un problème” par Stuart Marks:
Ceci est une impasse qui se produit parce que l’initialiseur statique du Fruit enum interagit mal avec l’initialisation de la classe.
Voir la spécification de 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 comme suit.
- Le thread principal référence la classe Fruit et lance le processus d’initialisation. Cela définit l’indicateur d’initialisation en cours et exécute l’initialiseur statique sur le thread principal.
- L’initialiseur statique exécute du code dans un autre thread et attend sa fin. Cet exemple utilise des stream parallèles, mais cela n’a rien à voir avec les stream en soi. Exécuter du code dans un autre thread par n’importe quel moyen, et attendre que ce code se termine, aura le même effet.
- Le code dans 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.)
- Le thread principal est bloqué en attendant que l’autre thread se termine, donc l’initialiseur statique ne se termine jamais. Comme l’indicateur d’initialisation en cours n’est pas effacé tant que l’initialiseur statique n’est pas terminé, 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 nécessitant l’initialisation de cette classe.
Fermer en tant que pas un problème.
Notez que FindBugs a un problème ouvert pour append un avertissement pour cette situation.
Pour ceux qui se demandent où sont les autres threads faisant référence à la classe Deadlock
, Java lambdas se comporte 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 Ssortingng[] 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 Ssortingng[] args) {} }
Il y a une excellente explication de ce problème par Andrei Pangin , datée du 7 avril 2015. Elle est disponible ici , mais elle est écrite 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 lors de l’initialisation de la classe.
Voici quelques citations de l’article:
Selon JLS , chaque classe possède un verrou d’initialisation unique qui est capturé lors de l’initialisation. Lorsqu’un autre thread tente d’accéder à cette classe lors de l’initialisation, il sera bloqué 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 sum 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(Ssortingng[] args) { System.out.println(SUM); } }
Maintenant, supprimez parallel()
ou remplacez lambda par Integer::sum
call – qu’est ce qui va changer?
Ici, nous voyons à nouveau l’impasse [il y avait quelques exemples de blocages dans les initialiseurs de classes précédemment dans l’article]. En raison des opérations de stream parallel()
exécutées dans un pool de threads distinct. Ces threads essaient d’exécuter le corps lambda, é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 la fin de l’initialiseur statique de classe, qui attend les résultats de la fin du stream.
Ce qui est plus époustouflant: ce code fonctionne différemment dans différents environnements. Il fonctionnera correctement sur une seule machine CPU et sera très probablement bloqué sur une machine multi-processeurs. 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