W3docs

Concorrência Estruturada em Java

Trate subtarefas concorrentes como uma unidade de trabalho em Java com concorrência estruturada (StructuredTaskScope).

Concorrência estruturada trata um grupo de subtarefas concorrentes como uma única unidade de trabalho: elas são lançadas juntas, terminam juntas e, se uma falhar ou o chamador for cancelado, as demais também são canceladas — nenhuma thread órfã sobrevive ao bloco que as iniciou. O modelo é fornecido por java.util.concurrent.StructuredTaskScope (uma API de prévia introduzida no Java 21) e se apoia nas mesmas threads virtuais abordadas anteriormente nesta parte. O objetivo é simples: tornar o código concorrente tão fácil de ler, depurar e raciocinar quanto um método sequencial comum.

Este capítulo explica por que "estruturado" importa, a anatomia de um escopo de tarefa, as duas políticas de encerramento integradas, como prazos e cancelamentos se propagam, e um exemplo prático executável. Assume-se que você esteja familiarizado com o framework executor e com Callable/Future.

Por que "estruturado"?

Os pools de threads clássicos são não estruturados: você envia uma tarefa com submit para um ExecutorService compartilhado e recebe um Future cujo tempo de vida não tem relação com o método que o criou. Uma tarefa pode sobreviver ao seu chamador, um erro em uma tarefa é invisível para suas irmãs, e o cancelamento precisa ser configurado manualmente. O resultado são threads vazando e tratamento de erros confuso.

A concorrência estruturada toma emprestada a disciplina do fluxo de controle estruturado: assim como um bloco try delimita suas instruções, um escopo de tarefa confina suas subtarefas. As subtarefas ramificadas dentro de um bloco devem todas ser concluídas antes que o bloco termine. Os tempos de vida se encaixam de forma limpa, de modo que um dump de thread e um rastreamento de pilha realmente mostram quem iniciou o quê.

PreocupaçãoNão estruturado (pool compartilhado ExecutorService)Estruturado (StructuredTaskScope)
Tempo de vida da subtarefaIndependente do chamadorLimitado pelo bloco envolvente
Erro em uma subtarefaOculto em um Future até você chamar getPode interromper todo o escopo
CancelamentoManual, fácil de esquecerAutomático em caso de falha ou interrupção
Limpeza de recursosPor sua contaclose() aguarda todas as subtarefas

A forma de um escopo

Um escopo é um AutoCloseable, portanto vive em um bloco try-with-resources. Você usa fork para criar subtarefas (cada uma retorna um identificador Subtask), chama join() para aguardá-las e, em seguida, lê cada resultado. A política ShutdownOnFailure cancela as subtarefas restantes no momento em que qualquer uma delas lança uma exceção:

import java.util.concurrent.StructuredTaskScope;

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    StructuredTaskScope.Subtask<String> user  = scope.fork(() -> fetchUser(id));
    StructuredTaskScope.Subtask<Integer> order = scope.fork(() -> fetchOrderCount(id));

    scope.join();            // wait for both branches
    scope.throwIfFailed();   // rethrow if either branch failed

    return new Profile(user.get(), order.get());
}   // close() guarantees both subtasks have ended before we leave

Se fetchUser lançar uma exceção, ShutdownOnFailure interrompe o fetchOrderCount ainda em execução, join() retorna e throwIfFailed() relança a causa original envolvida em um ExecutionException. Nenhuma thread é vazada.

Políticas de encerramento integradas

As duas políticas fornecidas cobrem os padrões mais comuns; você cria uma subclasse de StructuredTaskScope para qualquer outra necessidade.

PolíticaTermina quandoUse para
ShutdownOnFailureTodas têm êxito, ou uma falhaFan-out em que você precisa de cada resultado (o caso comum)
ShutdownOnSuccess<T>Primeiro êxito, ou todas falhamCompetição entre fontes redundantes; use a resposta mais rápida

ShutdownOnSuccess retorna o vencedor via result() e cancela os perdedores:

try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
    scope.fork(() -> queryMirrorA());
    scope.fork(() -> queryMirrorB());
    scope.join();
    return scope.result();   // the first one to return; the slower is cancelled
}

Prazos e cancelamentos se propagam

Um escopo pode ser aguardado com um prazo; quando ele expira, as subtarefas inacabadas são canceladas:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    scope.fork(() -> slowService());
    scope.joinUntil(Instant.now().plusSeconds(2));  // throws TimeoutException if late
    scope.throwIfFailed();
}

O cancelamento é cooperativo e flui para baixo: se a thread que possui o escopo for interrompida, todas as subtarefas são interrompidas em cascata. Como cada subtarefa é executada em sua própria thread virtual, criar milhares delas é barato — o escopo, e não um tamanho fixo de pool, é a unidade sobre a qual você raciocina.

Um exemplo prático: fan-out, falha e junção de uma lista

StructuredTaskScope é um recurso de prévia, portanto, para manter este exemplo executável em um JDK estável, modelamos a mesma ideia com um executor de thread virtual por tarefa: um bloco try-with-resources que delimita um grupo de subtarefas e só sai quando toda thread de subtarefa terminou. Ele distribui duas chamadas de forma concorrente, depois mostra como uma falha interrompe a unidade de trabalho e como invokeAll aguarda toda uma lista de uma só vez.

java— editable, runs on the server

O que observar na execução:

  • Ambas as subtarefas reportaram is virtual : true — cada submit foi executado em sua própria thread virtual, o mesmo portador leve que StructuredTaskScope.fork usa, portanto criar uma thread por subtarefa é barato.
  • O bloco do caminho feliz exibiu ran concurrently (<320ms): true mesmo que as duas buscas durmam 120ms e 200ms: elas se sobrepuseram, então o tempo real acompanha o ramo mais lento (~200ms), não a soma (320ms). Essa sobreposição é exatamente o ponto do fan-out.
  • Ao sair do bloco try-with-resources, close() foi chamado, bloqueando até que toda thread de subtarefa fosse encerrada — o escopo é a unidade de tempo de vida, exatamente a disciplina que StructuredTaskScope impõe por construção.
  • Na seção de falha, o programa exibiu caught: IllegalStateException -> upstream said no: um erro lançado dentro de uma subtarefa aparece no ponto de junção envolvido em ExecutionException, e getCause() devolve a exceção original.
  • Após capturar a falha, exibiu sibling cancelled: true — cancelamos o ramo good ainda em execução para que nenhum órfão sobrevivesse ao bloco, que é exatamente o que ShutdownOnFailure faz automaticamente; aqui fizemos manualmente para mostrar o mecanismo.

Tópicos relacionados

Prática

Prática
Com StructuredTaskScope.ShutdownOnFailure, o que acontece com as outras subtarefas ramificadas quando uma delas lança uma exceção?
Com StructuredTaskScope.ShutdownOnFailure, o que acontece com as outras subtarefas ramificadas quando uma delas lança uma exceção?
Was this page helpful?