Como informó InfoWorld, Java 22 introduce varias características nuevas relacionadas con subprocesos. Uno de los más importantes es el nuevo ScopedValue
sintaxis para tratar con valores compartidos en contextos multiproceso. Vamos a ver.
Simultaneidad estructurada y valores de alcance
ScopedValue
es una nueva forma de lograr ThreadLocal
-Comportamiento similar. Ambos elementos abordan la necesidad de crear datos que se compartan de forma segura dentro de un único hilo, pero ScopedValue
aspira a una mayor simplicidad. Está diseñado para funcionar en conjunto con Hilos virtuales y el nuevo Alcance de la tarea estructurada, que en conjunto simplifican el subprocesamiento y lo hacen más poderoso. A medida que estas nuevas funciones se utilizan con regularidad, la función de valores de ámbito tiene como objetivo abordar la mayor necesidad de gestionar el intercambio de datos dentro de los subprocesos.
ScopedValue como alternativa a ThreadLocal
En una aplicación multiproceso, a menudo necesitará declarar una variable que exista únicamente para el hilo actual. Piense en esto como algo similar a un Singleton (una instancia por aplicación), excepto que es una instancia por subproceso.
A pesar de ThreadLocal
Funciona bien en muchos casos, tiene limitaciones. Estos problemas se reducen al rendimiento de los subprocesos y a la carga mental del desarrollador. Es probable que ambos problemas aumenten a medida que los desarrolladores utilicen el nuevo VirtualThreads
presentar e introducir más hilos en sus programas. El JEP para valores de ámbito hace un buen trabajo al describir las limitaciones de los hilos virtuales.
A ScopedValue
instancia mejora lo que es posible usando ThreadLocal
de tres maneras:
- Es inmutable.
- Lo heredan los subprocesos secundarios.
- Se elimina automáticamente cuando se completa el método que lo contiene.
como el Especificación de valor de alcance dice:
La vida útil de estas variables por subproceso debe estar limitada: cualquier dato compartido a través de una variable por subproceso debería quedar inutilizable una vez que finalice el método que inicialmente compartió los datos.
Esto es diferente de ThreadLocal
referencias, que duran hasta que el hilo mismo termina o el ThreadLocal.remove()
Se llama al método.
La inmutabilidad hace que sea más fácil seguir la lógica de un ScopedValue
instancia y permite que la JVM la optimice agresivamente.
Los valores con alcance utilizan una devolución de llamada funcional (una lambda) para definir la vida de la variable, lo cual es un enfoque inusual en Java. Puede parecer extraño al principio, pero en la práctica funciona bastante bien.
Cómo utilizar una instancia de ScopedValue
Hay dos aspectos del uso de un ScopedValue
Ejemplo: proporcionar y consumir. Podemos ver esto en tres partes en el siguiente código.
Paso 1: declarar el ScopedValue
:
final static ScopedValue<...> MY_SCOPED_VALUE = ScopedValue.newInstance();
Paso 2: llene el ScopedValue
instancia:
ScopedValue.where(MY_SCOPED_VALUE, ACTUAL_VALUE).run(() -> {
/* ...Code that accesses ACTUAL_VALUE... */
});
Paso 3: consumir el ScopedValue
instancia (código que se llama en algún momento en el Paso 2):
var fooBar = DeclaringClass.MY_SCOPED_VALUE
La parte más interesante de este proceso es la llamada a ScopedValue.where()
. Esto le permite asociar el declarado ScopedValue
con un valor real, y luego llamar .run()
método, proporcionando una función de devolución de llamada que se ejecutará con el valor así definido para el ScopedValue
instancia.
Recuerde: El valor real asociado al ScopedValue
La instancia cambiará según el hilo que se esté ejecutando. ¡Por eso estamos haciendo todo esto! (La variable particular específica del hilo establecida en el ScopedValue
a veces se le llama su encarnación.)
Un ejemplo de código suele valer más que mil palabras, así que echemos un vistazo. En el siguiente código, creamos varios hilos y generamos un número aleatorio único para cada uno. Luego usamos un ScopedValue
instancia para aplicar ese valor a una variable asociada a un hilo:
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: %d\n", Thread.currentThread().getName(), RANDOM_NUMBER.get());
});
}).start();
}
}
}
La final estática ScopedValue<Integer> RANDOM_NUMBER = ScopedValue.newInstance();
la llamada nos da la RANDOM_NUMBER ScopedValue
para ser utilizado en cualquier parte de la aplicación. En cada hilo, generamos un número aleatorio y lo asociamos a RANDOM_NUMBER
.
Luego corremos hacia adentro. ScopedValue.where()
. Todo el código dentro del controlador se resolverá RANDOM_NUMBER
al específico establecido en el hilo actual. En nuestro caso, simplemente enviamos el hilo y su número a la consola.
Así es como se ve una carrera:
$ 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
Tenga en cuenta que actualmente necesitamos activar el enable-preview
cambie para ejecutar este código. Eso no será necesario una vez que se promueva la función de valores de ámbito.
Cada hilo obtiene una versión distinta de RANDOM_NUMBER
. Dondequiera que se acceda a ese valor, sin importar cuán profundamente esté anidado, obtendrá la misma versión, siempre que se origine desde dentro de ese valor. run()
llamar de vuelta.
En un ejemplo tan simple, puedes imaginar pasar el valor aleatorio como parámetro del método; sin embargo, a medida que el código de la aplicación crece, rápidamente se vuelve inmanejable y conduce a componentes estrechamente acoplados. Usando un ScopedValue
La instancia es una manera fácil de hacer que la variable sea universalmente accesible mientras la mantiene restringida a un valor determinado para el hilo actual.
Usando ScopedValue con StructuredTaskScope
Porque ScopedValue
tiene como objetivo facilitar el manejo de un gran número de subprocesos virtuales, y StructuredTaskScope
es una forma recomendada de utilizar hilos virtuales, necesitará saber cómo combinar estas dos características.
El proceso general de combinación ScopedValue
con StructuredTaskScope
es similar a nuestro anterior Thread
ejemplo; sólo difiere la sintaxis:
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: %d\n", Thread.currentThread().threadId(), RANDOM_NUMBER.get());
});
return null;
});
}
scope.join();
}
}
}
La estructura general es la misma: definimos el ScopedValue
y luego creamos hilos y usamos el ScopedValue (RANDOM_NUMBER)
en ellos. en lugar de crear Thread
objetos que utilizamos scope.fork()
.
Observe que devolvemos nulo de la lambda a la que pasamos scope.fork()
, porque en nuestro caso no estamos usando el valor de retorno; simplemente estamos enviando una cadena a la consola. Es posible devolver un valor de scope.fork()
y úsalo.
Además, tenga en cuenta que acabo de lanzar Exception
de main()
. El scope.fork()
El método arroja un InterruptedException
eso debe manejarse adecuadamente en el código de producción.
El ejemplo anterior es típico de ambos. StructuredTaskScope
y ScopedValue
. Como puede ver, funcionan bien juntos; de hecho, fueron diseñados para ello.
Valores con alcance en el mundo real
Hemos estado analizando ejemplos simples para ver cómo funciona la función de valores con alcance. Ahora pensemos en cómo funcionará en escenarios más complejos. En particular, el Valores de alcance JEP destaca el uso en una aplicación web grande donde interactúan muchos componentes. El componente de manejo de solicitudes puede ser responsable de obtener un objeto de usuario (un «principio») que representa la autorización para la solicitud actual. Este es un modelo de subproceso por solicitud utilizado por muchos marcos. Los subprocesos virtuales hacen que esto sea mucho más escalable al divorciar los subprocesos JVM de los subprocesos del sistema operativo.
Una vez que el controlador de solicitudes ha obtenido el objeto de usuario, puede exponerlo al resto de la aplicación con el ScopedValue
anotación. Luego, cualquier otro componente llamado desde el interior del where()
La devolución de llamada puede acceder al objeto de usuario específico del hilo. Por ejemplo, en el JEP, el ejemplo de código muestra una DBAccess
componente que depende de la PRINCIPAL
ScopedValue
para verificar la autorización:
/** 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(...);
}
}
Aquí puede ver los mismos esquemas que nuestro primer ejemplo. La única diferencia es que hay diferentes componentes. El serve()
Se imagina que el método crea subprocesos por solicitud y, en algún momento, el Application.handle()
la llamada interactuará con DBAccess.open()
. Debido a que la llamada se origina desde el interior del ScopedValue.where()
lo sabemos PRINCIPAL
se resolverá con el valor establecido para este hilo por el nuevo Principal(level)
llamar.
Otro punto importante es que las llamadas posteriores a scope.fork()
heredará el ScopedValue
instancias definidas por el padre. Así, por ejemplo, incluso si el serve()
método anterior era llamar scope.fork()
y acceder a la tarea secundaria dentro PRINCIPAL.get()
, obtendría el mismo valor de subproceso que el padre. (Puede ver el pseudocódigo de este ejemplo en la sección «Heredar valores de ámbito» del JEP).
La inmutabilidad de los valores de alcance significa que la JVM puede optimizar este intercambio de subprocesos secundarios, por lo que podemos esperar una sobrecarga de bajo rendimiento en estos casos.
Conclusión
Aunque la concurrencia multiproceso es intrínsecamente compleja, las características más nuevas de Java contribuyen en gran medida a hacerla más simple y potente. La nueva característica de valores de ámbito es otra herramienta eficaz para el conjunto de herramientas del desarrollador de Java. En total, Java 22 ofrece un enfoque interesante y radicalmente mejorado para los subprocesos en Java.
Copyright © 2024 IDG Communications, Inc.