O seguinte snippet de código executa dois threads, um é um timer simples que registra a cada segundo, o segundo é um loop infinito que executa uma operação restante:
public class TestBlockingThread {
private static final Logger LOGGER = LoggerFactory.getLogger(TestBlockingThread.class);
public static final void main(String[] args) throws InterruptedException {
Runnable task = () -> {
int i = 0;
while (true) {
i++;
if (i != 0) {
boolean b = 1 % i == 0;
}
}
};
new Thread(new LogTimer()).start();
Thread.sleep(2000);
new Thread(task).start();
}
public static class LogTimer implements Runnable {
@Override
public void run() {
while (true) {
long start = System.currentTimeMillis();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// do nothing
}
LOGGER.info("timeElapsed={}", System.currentTimeMillis() - start);
}
}
}
}
Isso fornece o seguinte resultado:
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=13331
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1006
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
Não entendo por que a tarefa infinita bloqueia todos os outros threads por 13,3 segundos. Tentei alterar as prioridades do thread e outras configurações, nada funcionou.
Se você tiver alguma sugestão para corrigir isso (incluindo ajustar as configurações de alternância de contexto do SO), entre em contato.
java
multithreading
kms333
fonte
fonte
-XX:+PrintCompilation
, recebo o seguinte no momento em que o atraso estendido termina: TestBlockingThread :: lambda $ 0 @ 2 (24 bytes) COMPILE SKIPPED: loop infinito trivial (tente novamente em camada diferente)-Djava.compiler=NONE
, isso não acontecerá.Respostas:
Depois de todas as explicações aqui (graças a Peter Lawrey ), descobrimos que a principal fonte dessa pausa é que o ponto seguro dentro do loop é atingido muito raramente, portanto leva muito tempo para parar todos os threads para a substituição do código compilado pelo JIT.
Mas decidi ir mais fundo e descobrir por que raramente o ponto seguro é alcançado. Achei um pouco confuso por que o salto traseiro do
while
loop não é "seguro" nesse caso.Então, convoco
-XX:+PrintAssembly
toda a sua glória para ajudarApós alguma investigação, descobri que, após a terceira recompilação do
C2
compilador lambda, jogamos fora as pesquisas do safepoint dentro do loop completamente.ATUALIZAR
Durante o estágio de criação de perfil, a variável
i
nunca foi vista igual a 0. É por isso queC2
otimizou especulativamente esse ramo, para que o loop fosse transformado em algo comoObserve que o loop infinito originalmente foi remodelado para um loop finito regular com um contador! Devido à otimização do JIT para eliminar pesquisas de ponto seguro em loops finitos, também não havia pesquisa de ponto seguro nesse loop.
Depois de algum tempo,
i
embrulhado de volta para0
, e a armadilha incomum foi tomada. O método foi desoptimizado e a execução continuou no intérprete. Durante a recompilação com um novo conhecimento,C2
reconheceu o loop infinito e desistiu da compilação. O restante do método prosseguiu no intérprete com pontos de segurança adequados.Há uma excelente postagem de blog de leitura obrigatória, "Pontos seguros: significado, efeitos colaterais e despesas gerais", de Nitsan Wakart, que aborda pontos seguros e esse problema em particular.
A eliminação do Safepoint em loops contados por muito tempo é conhecida por ser um problema. O bug
JDK-5014723
(graças a Vladimir Ivanov ) soluciona esse problema.A solução alternativa estará disponível até que o bug seja finalmente corrigido.
-XX:+UseCountedLoopSafepoints
(ele irá causar pena de desempenho global e pode levar a acidente JVMJDK-8161147
). Após o uso, oC2
compilador continua mantendo os pontos seguros nos saltos traseiros e a pausa original desaparece completamente.Você pode desabilitar explicitamente a compilação do método problemático usando
-XX:CompileCommand='exclude,binary/class/Name,methodName'
Ou você pode reescrever seu código adicionando o ponto seguro manualmente. Por exemplo,
Thread.yield()
ligar no final do ciclo ou até mudarint i
paralong i
(obrigado, Nitsan Wakart ) também corrigirá a pausa.fonte
-XX:+UseCountedLoopSafepoints
em produção, pois pode travar a JVM . Até agora, a melhor solução alternativa é dividir manualmente o loop longo em outros mais curtos.c2
remove safepoints! mas mais uma coisa que eu não entendi é o que está acontecendo a seguir. Até onde eu vejo, não há pontos seguros após o desenrolar do loop (?) e parece que não há como executar o stw. para que ocorra algum tempo limite e a des otimização ocorre?i
nunca é 0, então o loop é especulativamente transformado em algo comofor (int i = osr_value; i != 0; i++) { if (1 % i == 0) uncommon_trap(); } uncommon_trap();
um loop regular finito contado. Depois quei
volta para 0, a captura incomum é feita, o método é desoptimizado e prosseguido no intérprete. Durante a recompilação com o novo conhecimento, o JIT reconhece o loop infinito e desiste da compilação. O restante do método é executado no intérprete com pontos de segurança adequados.Em resumo, o loop que você possui não tem um ponto seguro dentro dele, exceto quando
i == 0
é atingido. Quando esse método é compilado e aciona o código a ser substituído, ele precisa levar todos os encadeamentos para um ponto seguro, mas isso leva muito tempo, bloqueando não apenas o encadeamento que executa o código, mas todos os encadeamentos na JVM.Eu adicionei as seguintes opções de linha de comando.
Também modifiquei o código para usar o ponto flutuante que parece levar mais tempo.
E o que eu vejo na saída é
Nota: para que o código seja substituído, os threads precisam ser interrompidos em um ponto seguro. No entanto, parece aqui que esse ponto seguro é alcançado muito raramente (possivelmente apenas quando
i == 0
Alterando a tarefa paraEu vejo um atraso semelhante.
Ao adicionar código ao loop com cuidado, você recebe um atraso maior.
fica
No entanto, altere o código para usar um método nativo que sempre tenha um ponto seguro (se não for um intrínseco)
impressões
Nota: adicionar
if (Thread.currentThread().isInterrupted()) { ... }
a um loop adiciona um ponto seguro.Nota: Isso aconteceu em uma máquina com 16 núcleos, portanto, não há falta de recursos da CPU.
fonte
Encontrou a resposta do porquê . Eles são chamados de pontos seguros e são mais conhecidos como o Stop-The-World que acontece por causa do GC.
Consulte este artigo: Registrando pausas de interrupção do mundo na JVM
Lendo o Glossário de Termos do HotSpot , ele define o seguinte:
Correndo com os sinalizadores mencionados acima, recebo esta saída:
Observe o terceiro evento STW:
Tempo total parado: 10.7951187 segundos Os
threads de parada demoraram: 10.7950774 segundos
O JIT em si praticamente não demorou, mas depois que a JVM decidiu executar uma compilação do JIT, entrou no modo STW, no entanto, como o código a ser compilado (o loop infinito) não possui um site de chamada , nenhum ponto seguro foi alcançado.
O STW termina quando o JIT acaba desistindo de esperar e conclui que o código está em um loop infinito.
fonte
Depois de seguir os tópicos de comentários e alguns testes por conta própria, acredito que a pausa é causada pelo compilador JIT. Por que o compilador JIT está demorando tanto tempo está além da minha capacidade de depurar.
No entanto, como você só pediu como evitar isso, eu tenho uma solução:
Puxe seu loop infinito para um método em que ele possa ser excluído do compilador JIT
Execute seu programa com este argumento da VM:
-XX: CompileCommand = exclude, PACKAGE.TestBlockingThread :: infLoop (substitua PACKAGE pelas informações do pacote)
Você deve receber uma mensagem como esta para indicar quando o método teria sido compilado por JIT:
### Excluindo a compilação: bloqueio estático.TestBlockingThread :: infLoop
você pode perceber que eu coloquei a classe em um pacote chamado blocking
fonte
i == 0
while
loop não é um ponto seguro?if (i != 0) { ... } else { safepoint(); }
mas isso é muito raro. ie se você sair / interromper o ciclo, obtém os mesmos tempos.