Conforme relatado no InfoWorld, Java 22 apresenta vários novos recursos relacionados a threads. Um dos mais importantes é o novo ScopedValue
sintaxe para lidar com valores compartilhados em contextos multithread. Vamos dar uma olhada.
Simultaneidade estruturada e valores com escopo definido
ScopedValue
é uma nova maneira de alcançar ThreadLocal
-comportamento semelhante. Ambos os elementos atendem à necessidade de criar dados que sejam compartilhados com segurança em um único thread, mas ScopedValue
busca maior simplicidade. Ele foi projetado para funcionar em conjunto com VirtualThreads e o novo StructuredTaskScope, que juntos simplificam o threading e o tornam mais poderoso. À medida que esses novos recursos passam a ser usados regularmente, o recurso de valores com escopo tem como objetivo atender à crescente necessidade de gerenciamento do compartilhamento de dados dentro de threads.
ScopedValue como alternativa ao ThreadLocal
Em um aplicativo multithread, muitas vezes você precisará declarar uma variável que exista exclusivamente para o thread atual. Pense nisso como algo semelhante a um Singleton – uma instância por aplicativo – exceto que é uma instância por thread.
Embora ThreadLocal
funciona bem em muitos casos, tem limitações. Esses problemas se resumem ao desempenho do thread e à carga mental do desenvolvedor. Ambos os problemas provavelmente aumentarão à medida que os desenvolvedores usarem o novo VirtualThreads
apresentam e introduzem mais threads em seus programas. O JEP para valores com escopo definido faz um bom trabalho ao descrever as limitações dos threads virtuais.
A ScopedValue
instância melhora o que é possível usando ThreadLocal
de três maneiras:
- É imutável.
- É herdado por threads filhos.
- Ele é descartado automaticamente quando o método que o contém é concluído.
Como diz a especificação ScopedValue:
O tempo de vida dessas variáveis por thread deve ser limitado: quaisquer dados compartilhados por meio de uma variável por thread devem se tornar inutilizáveis quando o método que inicialmente compartilhou os dados for concluído.
Isto é diferente de ThreadLocal
referências, que duram até o próprio thread terminar ou o ThreadLocal.remove()
método é chamado.
A imutabilidade torna mais fácil seguir a lógica de um ScopedValue
instância e permite que a JVM a otimize agressivamente.
Os valores com escopo usam um retorno de chamada funcional (um lambda) para definir a vida da variável, o que é uma abordagem incomum em Java. Pode parecer estranho no início, mas na prática funciona muito bem.
Como usar uma instância ScopedValue
Existem dois aspectos no uso de um ScopedValue
exemplo: fornecer e consumir. Podemos ver isso em três partes no código a seguir.
Etapa 1: declarar o ScopedValue
:
final static ScopedValue<...> MY_SCOPED_VALUE = ScopedValue.newInstance();
Etapa 2: preencher o ScopedValue
instância:
ScopedValue.where(MY_SCOPED_VALUE, ACTUAL_VALUE).run(() -> {
/* ...Code that accesses ACTUAL_VALUE... */
});
Etapa 3: consumir o ScopedValue
instância (código que é chamado em algum momento na etapa 2):
var fooBar = DeclaringClass.MY_SCOPED_VALUE
A parte mais interessante deste processo é o apelo à ScopedValue.where()
. Isso permite associar o declarado ScopedValue
com um valor real e, em seguida, chame .run()
método, fornecendo uma função de retorno de chamada que será executada com o valor definido para o ScopedValue
instância.
Lembre-se: O valor real associado ao ScopedValue
a instância mudará de acordo com o thread que está sendo executado. É por isso que estamos fazendo tudo isso! (A variável específica específica do thread definida no ScopedValue
às vezes é chamado de seu encarnação.)
Um exemplo de código geralmente vale mais que mil palavras, então vamos dar uma olhada. No código a seguir, criamos vários threads e geramos um número aleatório exclusivo para cada um. Usamos então um ScopedValue
instância para aplicar esse valor a uma variável associada ao thread:
import java.util.concurrent.ThreadLocalRandom;
public class Simple {
static final ScopedValue<Integer> RANDOM_NUMBER = ScopedValue.newInstance();
public static void main(String() args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
int randomNumber = ThreadLocalRandom.current().nextInt(1, 101);
ScopedValue.where(RANDOM_NUMBER, randomNumber).run(() -> {
System.out.printf("Thread %s: Random number: %dn", Thread.currentThread().getName(), RANDOM_NUMBER.get());
});
}).start();
}
}
}
A final estática ScopedValue<Integer> RANDOM_NUMBER = ScopedValue.newInstance();
chamada nos dá o RANDOM_NUMBER ScopedValue
para ser usado em qualquer lugar do aplicativo. Em cada thread, geramos um número aleatório e o associamos a RANDOM_NUMBER
.
Então, corremos para dentro ScopedValue.where()
. Todo o código dentro do manipulador será resolvido RANDOM_NUMBER
para aquele específico definido no thread atual. No nosso caso, apenas enviamos o thread e seu número para o console.
Esta é a aparência de uma corrida:
$ javac --release 23 --enable-preview Simple.java
Note: Simple.java uses preview features of Java SE 23.
Note: Recompile with -Xlint:preview for details.
$ java --enable-preview Simple
Thread Thread-1: Random number: 45
Thread Thread-2: Random number: 100
Thread Thread-3: Random number: 51
Thread Thread-4: Random number: 74
Thread Thread-5: Random number: 37
Thread Thread-0: Random number: 32
Thread Thread-6: Random number: 28
Thread Thread-7: Random number: 43
Thread Thread-8: Random number: 95
Thread Thread-9: Random number: 21
Observe que atualmente precisamos ativar o enable-preview
mude para executar este código. Isso não será necessário quando o recurso de valores com escopo definido for promovido.
Cada thread obtém uma versão distinta de RANDOM_NUMBER
. Onde quer que esse valor seja acessado, não importa quão profundamente aninhado, ele obterá a mesma versão – desde que se origine dentro desse valor. run()
ligar de volta.
Em um exemplo tão simples, você pode imaginar passar o valor aleatório como parâmetro do método; entretanto, à medida que o código do aplicativo cresce, isso rapidamente se torna incontrolável e leva a componentes fortemente acoplados. Usando um ScopedValue
instance é uma maneira fácil de tornar a variável universalmente acessível, mantendo-a restrita a um determinado valor para o thread atual.
Usando ScopedValue com StructuredTaskScope
Porque ScopedValue
tem como objetivo facilitar o tratamento de um grande número de threads virtuais e StructuredTaskScope
é uma forma recomendada de usar threads virtuais, você precisará saber como combinar esses dois recursos.
O processo geral de combinação ScopedValue
com StructuredTaskScope
é semelhante ao nosso anterior Thread
exemplo; apenas a sintaxe difere:
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.StructuredTaskScope;
public class ThreadScoped {
static final ScopedValue<Integer> RANDOM_NUMBER = ScopedValue.newInstance();
public static void main(String() args) throws Exception {
try (StructuredTaskScope scope = new StructuredTaskScope()) {
for (int i = 0; i < 10; i++) {
scope.fork(() -> {
int randomNumber = ThreadLocalRandom.current().nextInt(1, 101);
ScopedValue.where(RANDOM_NUMBER, randomNumber).run(() -> {
System.out.printf("Thread %s: Random number: %dn", Thread.currentThread().threadId(), RANDOM_NUMBER.get());
});
return null;
});
}
scope.join();
}
}
}
A estrutura geral é a mesma: definimos o ScopedValue
e então criamos threads e usamos o ScopedValue (RANDOM_NUMBER)
neles. Em vez de criar Thread
objetos, usamos scope.fork()
.
Observe que retornamos null do lambda para o qual passamos scope.fork()
, porque em nosso caso não estamos usando o valor de retorno – estamos apenas enviando uma string para o console. É possível retornar um valor de scope.fork()
e use-o.
Além disso, observe que acabei de lançar Exception
de main()
. O scope.fork()
método lança um InterruptedException
que deve ser tratado corretamente no código de produção.
O exemplo acima é típico de ambos StructuredTaskScope
e ScopedValue
. Como você pode ver, eles funcionam bem juntos – na verdade, foram projetados para isso.
Valores com escopo definido no mundo real
Vimos exemplos simples para ver como funciona o recurso de valores com escopo definido. Agora vamos pensar em como isso funcionará em cenários mais complexos. Em particular, o JEP de valores com escopo destaca o uso em uma grande aplicação web onde muitos componentes estão interagindo. O componente de tratamento de solicitações pode ser responsável por obter um objeto de usuário (um “princípio”) que representa a autorização para a solicitação atual. Este é um modelo de thread por solicitação usado por muitos frameworks. Os threads virtuais tornam isso muito mais escalável ao separar os threads da JVM dos threads do sistema operacional.
Depois que o manipulador de solicitações obtiver o objeto de usuário, ele poderá expô-lo ao restante da aplicação com o ScopedValue
anotação. Então, quaisquer outros componentes chamados de dentro do where()
retorno de chamada pode acessar o objeto de usuário específico do thread. Por exemplo, no JEP, o exemplo de código mostra um DBAccess
componente que depende do PRINCIPAL
ScopedValue
para verificar a autorização:
/** https://openjdk.org/jeps/429 */
class Server {
final static ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance();
void serve(Request request, Response response) {
var level = (request.isAdmin() ? ADMIN : GUEST);
var principal = new Principal(level);
ScopedValue.where(PRINCIPAL, principal)
.run(() -> Application.handle(request, response));
}
}
class DBAccess {
DBConnection open() {
var principal = Server.PRINCIPAL.get();
if (!principal.canOpen()) throw new InvalidPrincipalException();
return newConnection(...);
}
}
Aqui você pode ver os mesmos contornos do nosso primeiro exemplo. A única diferença é que existem componentes diferentes. O serve()
imagina-se que o método crie threads por solicitação e, em algum momento, o Application.handle()
chamada irá interagir com DBAccess.open()
. Como a chamada se origina de dentro do ScopedValue.where()
nós sabemos isso PRINCIPAL
será resolvido para o valor definido para este thread pelo novo Principal(level)
chamar.
Outro ponto importante é que as chamadas subsequentes para scope.fork()
herdará o ScopedValue
instâncias definidas pelo pai. Assim, por exemplo, mesmo que o serve()
método acima era chamar scope.fork()
e acesse a tarefa filho dentro PRINCIPAL.get()
, obteria o mesmo valor vinculado ao thread que o pai. (Você pode ver o pseudocódigo deste exemplo na seção “Herdando valores com escopo definido“ do JEP.)
A imutabilidade dos valores com escopo definido significa que a JVM pode otimizar esse compartilhamento de thread filho, portanto, podemos esperar uma sobrecarga de baixo desempenho nesses casos.
Conclusão
Embora a simultaneidade multithread seja inerentemente complexa, os recursos Java mais recentes contribuem bastante para torná-la mais simples e poderosa. O novo recurso de valores com escopo definido é outra ferramenta eficaz para o kit de ferramentas do desenvolvedor Java. Ao todo, Java 22 oferece uma abordagem interessante e radicalmente aprimorada para threading em Java.