Como se informó en InfoWorld, Java 22 introduce varias características nuevas relacionadas con subprocesos. Uno de los más importantes es la nueva sintaxis ScopedValue para tratar con valores compartidos en contextos multiproceso. Echemos un vistazo. Simultaneidad estructurada y valores de alcanceScopedValue es una nueva forma de lograr un comportamiento similar a ThreadLocal. Ambos elementos abordan la necesidad de crear datos que se compartan de forma segura dentro de un solo hilo, pero ScopedValue apunta a una mayor simplicidad. Está diseñado para funcionar en conjunto con VirtualThreads y el nuevo StructuredTaskScope, que juntos simplifican los subprocesos y los hacen más potentes. A medida que estas nuevas funciones se utilizan con regularidad, la función de valores de ámbito está destinada a 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 existe exclusivamente 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. Aunque 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 la nueva función VirtualThreads e introduzcan más subprocesos en sus programas. El JEP para valores con alcance hace un buen trabajo al describir las limitaciones de los subprocesos virtuales. Una instancia de ScopedValue mejora lo que es posible usar 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 dice la especificación ScopedValue: La vida útil de estas variables por subproceso debe estar limitada: cualquier dato compartido a través de una variable por subproceso debe quedar inutilizable una vez que finaliza el método que inicialmente compartió los datos. Esto es diferente de las referencias ThreadLocal, que duran hasta que finaliza el hilo o se llama al método ThreadLocal.remove(). La inmutabilidad facilita el seguimiento de la lógica de una instancia de ScopedValue 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 sonar extraño al principio, pero en la práctica funciona bastante bien. Cómo usar una instancia de ScopedValue Hay dos aspectos del uso de una instancia de ScopedValue: proporcionar y consumir. Podemos ver esto en tres partes en el siguiente código. Paso 1: Declarar el ScopedValue: ScopedValue estático final<...> MY_SCOPED_VALUE = ScopedValue.newInstance(); Paso 2: Complete la instancia de ScopedValue: ScopedValue.where(MY_SCOPED_VALUE, ACTUAL_VALUE).run(() -> { /* …Código que accede a ACTUAL_VALUE… */ }); Paso 3: Consumir la instancia de ScopedValue (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 ScopedValue declarado con un valor real y luego llamar al método .run(), proporcionando una función de devolución de llamada que se ejecutará con el valor así definido para la instancia de ScopedValue. Recuerde: el valor real asociado a la instancia de ScopedValue cambia según el hilo que se esté ejecutando. ¡Por eso estamos haciendo todo esto! (La variable particular específica del hilo establecida en ScopedValue a veces se denomina su encarnación). Un ejemplo de código a menudo vale 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 una instancia de ScopedValue para aplicar ese valor a una variable asociada a un subproceso: import java.util.concurrent.ThreadLocalRandom; clase pública simple { valor de alcance final estático RANDOM_NUMBER = ScopedValue.newInstance(); principal vacío estático público (cadena[] argumentos) { para (int i = 0; i < 10; i++) {
new Thread(() -> { int número aleatorio = ThreadLocalRandom.current().nextInt(1, 101); ScopedValue.where(RANDOM_NUMBER, randomNumber).run(() -> { System.out.printf(«Subproceso %s: Número aleatorio: %d\n», Thread.currentThread().getName(), RANDOM_NUMBER.get( )); }); }).comenzar(); } } } El ScopedValue final estático RANDOM_NUMBER = ScopedValue.newInstance(); La llamada nos proporciona el RANDOM_NUMBER ScopedValue que se utilizará en cualquier parte de la aplicación. En cada hilo, generamos un número aleatorio y lo asociamos a RANDOM_NUMBER. Luego, ejecutamos dentro de ScopedValue.where(). Todo el código dentro del controlador resolverá RANDOM_NUMBER en el 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 ejecución: $ javac –release 23 –enable-preview Simple.java Nota: Simple.java usa funciones de vista previa de Java SE 23. Nota: Vuelva a compilar con -Xlint:preview para obtener más detalles. $ java –enable-preview Subproceso simple Subproceso-1: Número aleatorio: 45 Subproceso Subproceso-2: Número aleatorio: 100 Subproceso Subproceso-3: Número aleatorio: 51 Subproceso Subproceso-4: Número aleatorio: 74 Subproceso Subproceso-5: Aleatorio número: 37 Hilo Hilo-0: Número aleatorio: 32 Hilo Hilo-6: Número aleatorio: 28 Hilo Hilo-7: Número aleatorio: 43 Hilo Hilo-8: Número aleatorio: 95 Hilo Hilo-9: Número aleatorio: 21 Tenga en cuenta que Actualmente necesitamos activar el interruptor de habilitar vista previa para ejecutar este código. Eso no será necesario una vez que se promueva la función de valores con alcance. Cada hilo obtiene una versión distinta de RANDOM_NUMBER. Dondequiera que se acceda a ese valor, no importa qué tan profundamente anidado, obtendrá esa misma versión, siempre y cuando se origine desde dentro de esa devolución de llamada run(). En un ejemplo tan simple, puedes imaginar pasar el valor aleatorio como un parámetro de 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. Usar una instancia de ScopedValue es una manera fácil de hacer que la variable sea universalmente accesible y al mismo tiempo mantenerla restringida a un valor determinado para el subproceso actual. Usar ScopedValue con StructuredTaskScope porque ScopedValue está destinado a facilitar el manejo de una gran cantidad de subprocesos virtuales, y StructuredTaskScope es una opción recomendada. Para utilizar subprocesos virtuales, necesitará saber cómo combinar estas dos características. El proceso general de combinar ScopedValue con StructuredTaskScope es similar a nuestro ejemplo de subproceso anterior; sólo difiere la sintaxis: import java.util.concurrent.ThreadLocalRandom; importar java.util.concurrent.StructuredTaskScope; clase pública ThreadScoped { valor de alcance final estático RANDOM_NUMBER = ScopedValue.newInstance(); principal vacío estático público (cadena[] args) lanza una excepción { try (alcance de StructuredTaskScope = new StructuredTaskScope()) { para (int i = 0; i < 10; i++) {
scope.fork(() -> { int número aleatorio = ThreadLocalRandom.current().nextInt(1, 101); ScopedValue.where(RANDOM_NUMBER, randomNumber).run(() -> { System.out.printf(«Subproceso %s: Número aleatorio: %d\n», Thread.currentThread().threadId(), RANDOM_NUMBER.get( )); }); devolver nulo; }); } alcance.join(); } } } La estructura general es la misma: definimos ScopedValue y luego creamos subprocesos y usamos ScopedValue (RANDOM_NUMBER) en ellos. En lugar de crear objetos Thread, usamos alcance.fork(). Observe que devolvemos nulo del lambda que pasamos a alcance.fork(), porque en nuestro caso no estamos usando el valor de retorno, solo estamos generando una cadena. a la consola. Es posible devolver un valor desde alcance.fork() y usarlo. Además, tenga en cuenta que acabo de lanzar una excepción desde main(). El método Scope.fork() genera una InterruptedException que debe manejarse correctamente en el código de producción. El ejemplo anterior es típico tanto de StructuredTaskScope como de 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 JEP de Scoped Values 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 separar los subprocesos de 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 la anotación ScopedValue. Luego, cualquier otro componente llamado desde dentro de la devolución de llamada where() puede acceder al objeto de usuario específico del hilo. Por ejemplo, en JEP, el ejemplo de código muestra un componente DBAccess que se basa en PRINCIPAL ScopedValue para verificar la autorización: /** https://openjdk.org/jeps/429 */ class Server { final static ScopedValue PRINCIPAL = ScopedValue.newInstance(); servicio vacío (solicitud de solicitud, respuesta de respuesta) { var nivel = (request.isAdmin()? ADMIN: INVITADO); var principal = nuevo Principal(nivel); ScopedValue.where(PRINCIPAL, principal) .run(() -> Application.handle(solicitud, respuesta)); } } clase DBAccess { DBConnection open() { var principal = Server.PRINCIPAL.get(); if (!principal.canOpen()) lanza una nueva InvalidPrincipalException(); devolver nuevaConexión(…); } } Aquí puede ver los mismos esquemas que en nuestro primer ejemplo. La única diferencia es que hay diferentes componentes. Se imagina que el método server() crea subprocesos por solicitud y, en algún momento, la llamada Application.handle() interactuará con DBAccess.open(). Debido a que la llamada se origina desde dentro de ScopedValue.where(), sabemos que PRINCIPAL se resolverá en el valor establecido para este hilo por la nueva llamada Principal(level). Otro punto importante es que las llamadas posteriores a alcance.fork() heredarán las instancias de ScopedValue definidas por el padre. Entonces, por ejemplo, incluso si el método de servicio() anterior llamara a alcance.fork() y accediera a la tarea secundaria dentro de PRINCIPAL.get(), obtendría el mismo valor vinculado al subproceso que el principal. (Puede ver el pseudocódigo de este ejemplo en la sección «Heredar valores de ámbito» del JEP). La inmutabilidad de los valores de ámbito 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ónAunque 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 conjunto, Java 22 ofrece un enfoque interesante y radicalmente mejorado para los subprocesos en Java. Copyright © 2024 IDG Communications, Inc.
Source link
Deja una respuesta