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 scénario de test de reproduction minimum pour ce comportement. Si je:
- mettre le bloc dans la méthode main au lieu d'un initialiseur statique,
- supprimer la parallélisation, ou
- retirer le lambda,
le code se termine instantanément. Quelqu'un peut-il expliquer ce comportement? Est-ce un bug ou est-ce voulu?
J'utilise OpenJDK version 1.8.0_66-internal.
i -> i
n'est pas une référence de méthode, il eststatic method
implémenté dans la classe Deadlock. Si remplaceri -> i
parFunction.identity()
ce code devrait être bien.Réponses:
J'ai trouvé un rapport de bogue d'un cas très similaire ( JDK-8143380 ) qui a été fermé comme «Pas un problème» par Stuart Marks:
J'ai pu trouver un autre rapport de bogue à ce sujet ( JDK-8136753 ), également fermé comme "Pas un problème" par Stuart Marks:
Notez que FindBugs a un problème ouvert pour l'ajout d'un avertissement pour cette situation.
la source
this
échapper lors de la construction de l'objet. La règle de base est de ne pas utiliser d'opérations multithreads dans les initialiseurs. Je ne pense pas que ce soit difficile à comprendre. Votre exemple d'enregistrement d'une fonction implémentée lambda dans un registre est une chose différente, il ne crée pas de blocages à moins que vous n'attendiez l'un de ces threads d'arrière-plan bloqués. Néanmoins, je déconseille fortement d'effectuer de telles opérations dans un initialiseur de classe. Ce n'est pas ce à quoi ils sont destinés.Pour ceux qui se demandent où sont les autres threads référençant la
Deadlock
classe elle-même, les lambdas Java 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) {} }
la source
lambda1
cet exemple). Mettre chaque lambda dans sa propre classe aurait été beaucoup plus cher.i -> i
; ce ne sera pas la norme. Les expressions Lambda peuvent utiliser tous les membres de leur classe environnante, y comprisprivate
certains, ce qui fait de la classe de définition elle-même leur place naturelle. Laisser tous ces cas d'utilisation souffrir d'une implémentation optimisée pour le cas particulier des initialiseurs de classe avec une utilisation multithread d'expressions lambda triviales, n'utilisant pas les membres de leur classe de définition, n'est pas une option viable.Il y a une excellente explication de ce problème par Andrei Pangin , datée du 07 avril 2015. Elle est disponible ici , mais elle est écrite en russe (je suggère quand même de revoir les échantillons de code - 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 pendant l'initialisation, il sera bloqué sur le verrou jusqu'à ce que l'initialisation soit terminée. 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 nombres entiers, que doit-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); } }
Supprimer maintenant
parallel()
ou remplacez lambda parInteger::sum
call - qu'est-ce qui va changer?Ici, nous voyons à nouveau des blocages [il y avait quelques exemples de blocages dans les initialiseurs de classe précédemment dans l'article]. En raison des
parallel()
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 queprivate static
méthode à l'intérieur de laStreamSum
classe. 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 flux.Quoi de plus époustouflant: ce code fonctionne différemment dans différents environnements. Il fonctionnera correctement sur une seule machine à processeur et sera probablement suspendu à une machine à plusieurs processeurs. Cette différence vient 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
la source