Diretório de artigos
Notas após a leitura de "The Art of Java Concurrent Programming" - Explicação detalhada de FutureTask (Capítulo 10)
1. Introdução ao FutureTask
Além de implementar a interface, FutureTask Future
também implementa Runnable
a interface. Portanto, FutureTask pode ser entregue ao Executor para execução ou pode ser executado diretamente pelo thread de chamada ( FutureTask.run()
).
De acordo com o tempo em que o método FutureTask.run() é executado, a FutureTask pode estar nos três estados a seguir:
- Não iniciado . Antes da execução do método FutureTask.run(), a FutureTask não é iniciada. Quando uma FutureTask é criada e o método FutureTask.run() não é executado, a FutureTask não é iniciada.
- começou . Durante a execução do método FutureTask.run(), a FutureTask está no estado iniciado.
- concluído . O método FutureTask.run() termina normalmente após a execução, ou é cancelado (FutureTask.cancel(…)), ou uma exceção é lançada quando o método FutureTask.run() é executado e termina de forma anormal, e o FutureTask está em uma conclusão estado.
FutureTask
Um diagrama esquemático da transição de estado:
Para o método FutureTask.get() :
- Quando o FutureTask não é iniciado ou iniciado, a execução do método FutureTask.get() fará com que o thread de chamada seja bloqueado
- Quando FutureTask está no estado concluído, a execução do método FutureTask.get() fará com que o thread de chamada retorne o resultado imediatamente ou lance uma exceção.
Para o método FutureTask.cancel() :
- Quando a FutureTask não for iniciada, a execução do método FutureTask.cancel() fará com que a tarefa nunca seja executada
- Quando o FutureTask está no estado iniciado, a execução do método FutureTask.cancel(true) tentará parar a tarefa interrompendo o thread que está executando a tarefa
- Quando o FutureTask está no estado iniciado, a execução do método FutureTask.cancel(false) não afetará o thread que está executando a tarefa (deixe a tarefa em execução ser concluída)
- Quando a FutureTask estiver no estado concluído, a execução do método FutureTask.cancel(…) retornará false.
get
Diagrama esquemático do método e cancel
execução do método:
2. O uso de FutureTask
Você pode entregar o FutureTask para o Executor para execução, você também pode retornar um FutureTask através do método ExecutorService.submit(…) e então executar o método FutureTask.get() ou o método FutureTask.cancel(…). Além disso, você também pode usar o FutureTask sozinho
Quando um thread precisa esperar que outro thread conclua uma tarefa antes de continuar a execução, você pode usar FutureTask
por exemplo: Assumindo que existem vários threads executando várias tarefas, cada tarefa pode ser executada apenas uma vez, no máximo. Quando vários threads tentam executar a mesma tarefa ao mesmo tempo, apenas um thread tem permissão para executar a tarefa e os outros threads precisam aguardar a execução da tarefa antes de continuar.
código mostra como abaixo:
/**
* @author xppll
* @date 2022/1/18 16:21
*/
public class FutureTaskTest {
private final ConcurrentMap<Object, Future<String>> taskCache
= new ConcurrentHashMap<Object, Future<String>>();
private String executionTask(final String taskName) {
while (true) {
Future<String> future = taskCache.get(taskName);
if (future == null) {
Callable<String> task = new Callable<String>() {
@Override
public String call() throws Exception {
return taskName;
}
};
FutureTask<String> futureTask = new FutureTask<String>(task);
future = taskCache.putIfAbsent(taskName, futureTask);
if (future == null) {
future = futureTask;
futureTask.run();
}
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
taskCache.remove(taskName, future);
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
}
}
O diagrama esquemático da execução do código acima é o seguinte:
Quando dois encadeamentos tentam executar a mesma tarefa ao mesmo tempo, se o encadeamento 1 executar 1.3 e o encadeamento 2 executar 2.1, o encadeamento 2 aguardará em 2.2 até que o encadeamento 1 execute 1.4 antes que o encadeamento 2 possa iniciar a partir de 2.2 (FutureTask.get( ) )retornar
3. Implementação do FutureTask
FutureTask
A implementação é baseada em AbstractQueuedSynchronizer
(AQS). AQS é uma estrutura de sincronização que fornece mecanismos gerais para gerenciar atomicamente o estado de sincronização, bloquear e ativar threads e manter filas de threads bloqueados.
Para AQS detalhado, consulte: Notas após a leitura de "A Arte da Programação Concorrente em Java" - Capítulo 5 Bloqueios em Java
Todo sincronizador implementado com base no AQS conterá dois tipos de operações:
- pelo menos uma
acquire
ação.- Esta operação bloqueia o thread de chamada, a menos que/até que o estado do AQS permita que o thread continue em execução.
- A operação de aquisição de FutureTask é uma chamada de método get()/get(long timeout, TimeUnit unit).
- pelo menos uma
release
ação.- Esta operação altera o estado do AQS, o que permite que um ou mais threads bloqueados sejam desbloqueados.
- A operação de liberação de FutureTask inclui o método run() e o método cancel(...).
baseado emComposição sobre herançaDe acordo com o princípio, FutureTask declara uma subclasse privada interna herdada de AQS Sync
, e as chamadas para todos os métodos públicos de FutureTask serão delegadas a esta subclasse interna.
AQS é fornecida como a classe básica do "padrão de método modelo" para a subclasse interna Sync de FutureTask. Esta subclasse interna precisa apenas implementar os métodos de verificação de status e atualização de status , que controlarão as operações de aquisição e liberação de FutureTask. Especificamente, o Sync implementa tryAcquireShared(int)
os métodos e tryReleaseShared(int)
métodos do AQS, e o Sync usa esses dois métodos para verificar e atualizar o status da sincronização.
O diagrama de design do FutureTask é mostrado na figura:
Como pode ser visto na figura: Sync é uma classe privada interna de FutureTask, que herda de AQS. Ao criar um FutureTask, um Sync de objeto membro privado interno é criado e todos os métodos públicos do FutureTask são delegados diretamente ao Sync privado interno
Em seguida, vamos ver o processo de execução de vários métodos.
FutureTask.get()
O método chamaráAQS.acquireSharedInterruptibly(int arg)
o método, o processo específico é o seguinte:
-
Chame o método AQS.acquireSharedInterruptably(int arg) Esse método primeiro chamará de volta o método tryAcquireShared() implementado na subclasse Sync para determinar se a operação de aquisição pode ser bem-sucedida.
A condição para que a operação de aquisição seja bem-sucedida é: o estado é o estado de conclusão da execução RAN ou o estado cancelado CANCELLED e o executor não é nulo.
-
O método get() retorna imediatamente se for bem-sucedido. Se falhar, vá para a fila de espera do encadeamento para aguardar que outros encadeamentos executem a operação de liberação.
-
Quando outros threads executam a operação de liberação (como FutureTask.run() ou FutureTask.cancel()) para ativar o thread atual, o thread atual executa tryAcquireShared() novamente e retorna um valor positivo de 1, e o thread atual deixa a fila de espera do thread e ativa seu Thread sucessor (aqui produzirá o efeito de ativação em cascata, que será introduzido posteriormente)
-
Por fim, retorne o resultado calculado ou lance uma exceção.
FutureTask.run()
O processo de execução é o seguinte:
- Executa a tarefa especificada no construtor (Callable.call()).
- Atualize o estado de sincronização de forma atômica (chame AQS.compareAndSetState(int expect, int update), defina o estado para o estado de conclusão da execução RAN). Se a operação atômica for bem-sucedida, defina o valor da variável result que representa o resultado do cálculo para o valor de retorno de Callable.call() e, em seguida, chame AQS.releaseShared(int arg).
- AQS.releaseShared(int arg) primeiro chamará de volta o tryReleaseShared(arg) implementado na subclasse Sync para executar a operação de liberação (defina o executor de thread executando a tarefa como nulo e, em seguida, retorne true); AQS.releaseShared(int arg) e, em seguida, ative o encadeamento aguardando o primeiro encadeamento na fila.
- Chame FutureTask.done().
Quando o método FutureTask.get() é executado, se o FutureTask não estiver no estado de conclusão da execução RAN ou no estado cancelado CANCELLED, o thread de execução atual aguardará na fila de espera do AQS (consulte os threads A, B, C e D na figura abaixo). Quando um thread executa o método FutureTask.run() ou o método FutureTask.cancel(...), ele ativará o primeiro thread na fila de espera do thread (o thread E ativa o thread A mostrado na figura abaixo).
Assumindo que o FutureTask não foi iniciado ou iniciado no início, já existem 3 threads (A, B e C) aguardando na fila de espera. Neste ponto, a thread D executando o método get() fará com que a thread D também espere na fila de espera.
Quando o thread E executa o método run(), ele ativa o primeiro thread A na fila. Após o encadeamento A ser ativado, ele primeiro se remove da fila, depois ativa seu encadeamento sucessor B e, finalmente, o encadeamento A retorna do método get(). Os threads B, C e D repetem o fluxo de processamento do thread A. Eventualmente, todos os threads esperando na fila são ativados pela cascata e retornam do método get().