Eu me deparei com uma situação estranha em que usar um fluxo paralelo com um lambda em um inicializador estático leva uma eternidade sem a utilização da CPU. Aqui está o código:
class Deadlock {
static {
IntStream.range(0, 10000).parallel().map(i -> i).count();
System.out.println("done");
}
public static void main(final String[] args) {}
}
Este parece ser um caso de teste de reprodução mínima para esse comportamento. Se eu:
- coloque o bloco no método principal em vez de um inicializador estático,
- remova a paralelização ou
- remova o lambda,
o código é concluído instantaneamente. Alguém pode explicar esse comportamento? É um bug ou é intencional?
Estou usando o OpenJDK versão 1.8.0_66-internal.
i -> i
não é uma referência de método, éstatic method
implementado na classe Deadlock. Se substituiri -> i
porFunction.identity()
este código deve estar bem.Respostas:
Encontrei um relatório de bug de um caso muito semelhante ( JDK-8143380 ) que foi fechado como "Não é um problema" por Stuart Marks:
Consegui encontrar outro relatório de bug ( JDK-8136753 ), também fechado como "Not an Issue" por Stuart Marks:
Observe que FindBugs tem um problema aberto para adicionar um aviso para esta situação.
fonte
this
escapar durante a construção do objeto. A regra básica é: não use operações multithread nos inicializadores. Não acho que seja difícil de entender. Seu exemplo de registrar uma função implementada lambda em um registro é uma coisa diferente, ele não cria deadlocks a menos que você vá esperar por um desses threads de fundo bloqueados. No entanto, eu desencorajo fortemente a realização de tais operações em um inicializador de classe. Não é para isso que eles foram feitos.Para aqueles que estão se perguntando onde estão os outros threads que fazem referência à
Deadlock
própria classe, os lambdas do Java se comportam como você escreveu isto: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) {} }
Com classes anônimas regulares, não há impasse:
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) {} }
fonte
lambda1
i neste exemplo). Colocar cada lambda em sua própria classe teria sido consideravelmente mais caro.i -> i
; eles não serão a norma. As expressões lambda podem usar todos os membros de sua classe circundante, incluindoprivate
alguns, e isso torna a própria classe definidora seu lugar natural. Deixar todos esses casos de uso sofrerem de uma implementação otimizada para o caso especial de inicializadores de classe com uso multi-thread de expressões lambda triviais, não usando membros de sua classe definidora, não é uma opção viável.Há uma excelente explicação desse problema por Andrei Pangin , datada de 07 de abril de 2015. Está disponível aqui , mas está escrita em russo (sugiro revisar os exemplos de código de qualquer maneira - eles são internacionais). O problema geral é um bloqueio durante a inicialização da classe.
Aqui estão algumas citações do artigo:
De acordo com o JLS , cada classe possui um bloqueio de inicialização exclusivo que é capturado durante a inicialização. Quando outro encadeamento tenta acessar essa classe durante a inicialização, ele será bloqueado no bloqueio até que a inicialização seja concluída. Quando as classes são inicializadas simultaneamente, é possível obter um deadlock.
Escrevi um programa simples que calcula a soma de inteiros, o que deve imprimir?
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); } }
Agora remova
parallel()
ou substitua lambda porInteger::sum
chamada - o que mudará?Aqui, vemos o deadlock novamente [houve alguns exemplos de deadlocks em inicializadores de classe anteriormente no artigo]. Por causa das
parallel()
operações de fluxo executadas em um conjunto de encadeamentos separado. Esses threads tentam executar o corpo lambda, que é escrito em bytecode como umprivate static
método dentro daStreamSum
classe. Mas este método não pode ser executado antes da conclusão do inicializador estático da classe, que aguarda os resultados da conclusão do fluxo.O que é mais impressionante: este código funciona de maneira diferente em ambientes diferentes. Ele funcionará corretamente em uma máquina com uma única CPU e provavelmente travará em uma máquina com várias CPUs. Essa diferença vem da implementação do pool Fork-Join. Você pode verificar você mesmo alterando o parâmetro
-Djava.util.concurrent.ForkJoinPool.common.parallelism=N
fonte