Estou usando o AdoptOpenJDK jdk81212-b04
no Ubuntu Linux, executando o Eclipse 4.13. Eu tenho um método no Swing que cria uma lambda dentro de uma lambda; ambos provavelmente são chamados em threads separados. É assim (pseudocódigo):
private SwingAction createAction(final Data payload) {
System.out.println(System.identityHashCode(payload));
return new SwingAction(() -> {
System.out.println(System.identityHashCode(payload));
//do stuff
//show an "are you sure?" modal dialog and get a response
//show a file selection dialog
//when the dialog completes, create a worker and show a status:
showStatusDialogWithWorker(() -> new SwingWorker() {
protected String doInBackground() {
save(payload);
}
});
Você pode ver que as lambdas têm várias camadas de profundidade e, eventualmente, a "carga útil" capturada é salva em um arquivo, mais ou menos.
Mas antes de considerar as camadas e os threads, vamos diretamente ao problema:
- Na primeira vez que ligo
createAction()
, os doisSystem.out.println()
métodos imprimem exatamente o mesmo código hash, indicando que a carga capturada dentro do lambda é a mesma para a qual passeicreateAction()
. - Se eu ligar mais tarde
createAction()
com uma carga útil diferente, os doisSystem.out.println()
valores impressos serão diferentes! Em particular, a segunda linha impressa sempre indica o mesmo valor que foi impresso na etapa 1 !!
Eu posso repetir isso repetidamente; a carga útil transmitida continuará recebendo um código hash de identidade diferente, enquanto a segunda linha impressa (dentro do lambda) permanecerá a mesma! Eventualmente, algo clicará e, de repente, os números serão os mesmos novamente, mas eles divergirão com o mesmo comportamento.
De alguma forma, o Java está armazenando em cache o lambda, bem como o argumento que está indo para o lambda? Mas como isso é possível? O payload
argumento é marcado final
e, além disso, as capturas de lambda precisam ser efetivamente finais de qualquer maneira!
- Existe um bug do Java 8 que não reconhece um lambda não deve ser armazenado em cache se a variável capturada tiver vários lambdas de profundidade?
- Existe um bug do Java 8 que está armazenando em cache argumentos lambdas e lambda entre threads?
- Ou existe algo que eu não entendo sobre argumentos do método de captura lambda versus variáveis locais do método?
Primeira falha na tentativa de solução alternativa
Ontem, pensei em impedir esse comportamento simplesmente capturando o parâmetro method localmente na pilha de métodos:
private SwingAction createAction(final Data payload) {
final Data theRealPayload = payload;
System.out.println(System.identityHashCode(theRealPayload));
return new SwingAction(() -> {
System.out.println(System.identityHashCode(theRealPayload));
//do stuff
//show an "are you sure?" modal dialog and get a response
//show a file selection dialog
//when the dialog completes, create a worker and show a status:
showStatusDialogWithWorker(() -> new SwingWorker() {
protected String doInBackground() {
save(theRealPayload);
}
});
Com essa linha única Data theRealPayload = payload
, se, a partir de agora, eu usei em theRealPayload
vez de payload
repentinamente o bug não aparecer mais, e toda vez que liguei createAction()
, as duas linhas impressas indicariam exatamente a mesma instância da variável capturada.
No entanto, hoje essa solução alternativa parou de funcionar.
Problema separado de endereços de correção de bug; Mas por que?
Eu encontrei um bug separado que estava lançando uma exceção dentro showStatusDialogWithWorker()
. Basicamente, showStatusDialogWithWorker()
é suposto criar o trabalhador (no lambda passado) e mostrar uma caixa de diálogo de status até que o trabalhador seja concluído. Houve um erro que criaria o trabalhador corretamente, mas não conseguiu criar a caixa de diálogo, lançando uma exceção que surgiria e nunca seria pego. Corrigi esse bug para que o showStatusDialogWithWorker()
diálogo seja exibido com êxito quando o trabalhador estiver em execução e depois o feche após a conclusão do trabalhador. Agora não posso mais reproduzir o problema de captura lambda.
Mas por que algo interno se showStatusDialogWithWorker()
relaciona com o problema? Quando eu estava imprimindo System.identityHashCode()
fora e dentro do lambda, e os valores eram diferentes, isso estava acontecendo antes de showStatusDialogWithWorker()
ser chamado e antes da exceção ser lançada. Por que uma exceção posterior fará diferença?
Além disso, permanece a questão fundamental: como é possível que um final
parâmetro passado por um método e capturado por um lambda possa mudar?
final
argumento de método pode mudar fora e dentro do lambda? E por que capturar a variável comofinal
variável local na pilha faria alguma diferença?final
variável na pilha não resolve mais o problema. Eu continuo investigando.SwingAction
própria. O que você faz com osSwingAction
retornados do método?Respostas:
Não é. Como você apontou, a menos que haja um erro na JVM, isso não pode acontecer.
Isso é muito difícil de definir sem um exemplo reprodutível mínimo. Aqui estão as observações que você fez:
Uma explicação possível que se ajusta à evidência é que o lambda chamado de segunda vez é de fato o lambda da primeira execução e o lambda que foi criado pela segunda execução foi descartado. Isso daria as observações acima e colocaria o erro dentro do código que você não mostrou aqui.
Talvez você possa adicionar algum log extra para registrar: a) os IDs de qualquer lambda criado dentro de createAction no momento da criação (acho que você precisaria alterar os lambdas em classes anon que implementam as interfaces de retorno de chamada com o log em seus construtores) b) ids das lambdas no momento de sua invocação
Eu acho que o registro acima seria suficiente para provar ou refutar minha teoria.
GL!
fonte
Não encontro nada errado com as lambdas capturadas no seu código.
Sua solução alternativa não altera a captura local, pois você acabou de declarar uma nova variável e atribuí-la à mesma referência.
O problema é mais provável no manuseio do
SwingAction
objeto criado. Não ficaria surpreso se você descobrir que a impressão do IdentityHashCode desse objeto retornado em que você está usando gera valores consistentes com suas cargas úteis. Ou, em outras palavras, você pode estar usando uma referência anterior aSwingAction
.Isso não deve ser possível no nível de referência, a variável não pode ser reatribuída. A referência passada pode ser mutável, mas isso não se aplica a este caso.
fonte