Ir al contenido principal

Buscar en CooLWJ

选择搜索范围,输入关键词后按回车。

Idioma

Contexto de los hilos de Java

ThreadLocal es una clase de utilidad bastante habitual del paquete java.lang del JDK que implementa la funcionalidad de contexto de hilo. Su aplicación más común probablemente sea almacenar información clave, como el contexto del usuario después de iniciar sesión.

El contexto de los hilos también permite resolver con elegancia muchos problemas que parecen bastante complejos, por ejemplo, la integración de los marcos spring y mybatis:
Al desarrollar dentro del ecosistema de spring, normalmente delegamos en spring la gestión de las transacciones y utilizamos la anotación transactional para configurar su inicio, el nivel de aislamiento y otros datos. Tampoco accedemos a la base de datos directamente mediante jdbc, sino a través del framework mybatis. spring y mybatis son dos frameworks completamente independientes; sin embargo, al utilizarlos, parece que mybatis puede obtener y usar directamente la connection de base de datos abierta por spring. Prácticamente no tenemos que hacer nada: basta con añadir el paquete jar mybatis-spring-boot-stater.
La clave para implementar este elegante diseño es ThreadLocal. A grandes rasgos, el paquete JAR stater termina incorporando un MybatisAutoConfiguration y se inyecta mediante SPI en el contenedor de beans de nuestro servicio, en MybatisAutoConfiguration ya está configurado en mybatis necesario SqlSessionFactory, y se inyectó el propio componente de Spring SpringManagedTransactionFactory(implementa TransactionFactory,la fábrica de transacciones es un mecanismo de extensión proporcionado por mybatis que permite delegar la gestión de transacciones).
El flujo general de ejecución consiste en que el método con la anotación transactional termina siendo interceptado por un proxy AOP. En el advice previo, la clase proxy obtiene una connection de dataSource y acaba guardándola en un ThreadLocal (en realidad, lo que se guarda en ThreadLocal es un map cuya clave es dataSource y cuyo valor es connectionHolder, equivalente a connection); posteriormente, cuando mybatis ejecuta SQL, primero pasa por TransactionFactory obtener connection, porque lo que se inyecta en este momento TransactionFactory Sí SpringManagedTransactionFactory,por lo que se obtiene de ThreadLocal la connection correspondiente al dataSource.

Breve introducción al funcionamiento de ThreadLocal:

tapd_50394669_1660562598_27.png

ThreadLocalMap Es una tabla hash que resuelve las colisiones mediante sondeo lineal; se genera una para cada hilo, y cada objeto ThreadLocal contiene un threadLocalHashCode Se utiliza para calcular la clave hash.
Comparemos con HashMap,ThreadLocalMap La elección del sondeo lineal en lugar del encadenamiento como método de resolución de colisiones podría basarse en las siguientes consideraciones:
1、Consideraciones de memoria: debido a sus diferentes casos de uso, ThreadLocalMap suele contener muchos menos datos que HashMap; con pocos datos, el método de encadenamiento añade un consumo considerable de espacio para punteros
2、Eficiencia: partiendo de 1, cuando hay pocos elementos, el espacio ahorrado puede utilizarse para generar una tabla hash con un factor de carga menor, reducir las colisiones y mejorar la velocidad media de búsqueda

Diagrama de relaciones entre ThreadLocal, los hilos y ThreadLocalMap

Problema
A En los escenarios de tareas basados en grupos de hilos existe un problema de propagación del contexto, especialmente en el modelo de programación web basado en Tomcat. Como el propio Tomcat utiliza un grupo de hilos para gestionar las interacciones de solicitud y respuesta entre el servidor y el cliente, cada aplicación web tiene que encargarse manualmente de propagar correctamente el contexto de los hilos.
B Problema de eficiencia: si hay demasiados ThreadLocal en la aplicación y la tabla hash aumenta de tamaño, la velocidad de búsqueda disminuirá

Para el problema A, las soluciones habituales son bastante similares; voy a pegar directamente el código de nuestro sistema
2.png
Esta solución presenta dos problemas:
1、Si más adelante se añade un ThreadLocal que deba transmitirse correctamente, será necesario modificar el código para incluirlo en la lógica de wrap; cualquier omisión provocará un error
2、En algunas situaciones, el propio hilo que envía la tarea puede ejecutarla, por ejemplo:

  • Cuando el grupo de hilos está lleno y su RejectedExecutionHandler lo que se utiliza es CallerRunsPolicy , las tareas enviadas al grupo de hilos se ejecutan directamente en el hilo que las envía; en ese caso, la lógica de limpieza final provoca la pérdida del contexto de dicho hilo

  • Del mismo modo, al utilizar ForkJoinPool (incluye la ejecución en paralelo Stream y CompletableFuture , utiliza internamente ForkJoinPool ), lo que se expande es ForkJoinTask Se ejecutará directamente en el hilo que envía la tarea. La limpieza final también provocará la pérdida del contexto de ese hilo

Solución:
Problema 1:si la clase ThreadLocal ofreciera una API que permitiera obtener el número total de objetos ThreadLocal existentes en la aplicación en ese momento, el problema se resolvería fácilmente; por desgracia, no la ofrece. Si se quisiera hacerlo de todos modos, se podría obtener mediante reflexión lo que contiene el objeto thread en su inheritableThreadLocals(es simplemente un ThreadLocalMap)y después recorrer la tabla hash y extraer todas sus entradas para obtener todos los pares clave-valor de threadlocal. Otra posibilidad es añadir una capa de encapsulación a ThreadLocal del JDK para incorporar las funciones anteriores.
Problema 2:puede resolverse mejorando la lógica de wrap: al comenzar la ejecución del hilo, primero se obtiene y guarda temporalmente su contexto actual; después se sustituye por el contexto del hilo que envió la tarea. Antes de terminar la ejecución, en lugar de borrarlo, se restaura el contexto original del hilo utilizando los datos guardados.

transmittable-thread-local
Ya existe una solución hecha; su implementación parte de los dos enfoques anteriores y, además, se le ha dado un nombre a este proceso  CRR(capture/replay/restore) modo。
Hay tres formas de uso:
1 ### Modificación Runnable y Callable

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();  
// =====================================================  
// 在父线程中设置 
context.set("value-set-in-parent"); 
Runnable task = new RunnableTask(); 
// 额外的处理,生成修饰了的对象ttl
Runnable Runnable ttlRunnable = TtlRunnable.get(task); 
executorService.submit(ttlRunnable);  
// =====================================================  
// Task中可以读取,值是"value-set-in-parent" 
String value = context.get();

2 Decorar el pool de hilos

ExecutorService executorService = ... 
// 额外的处理,生成修饰了的对象
executorService executorService = TtlExecutors.getTtlExecutorService(executorService);  
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();  
// =====================================================  
// 在父线程中设置 
context.set("value-set-in-parent");  
Runnable task = new RunnableTask(); 
Callable call = new CallableTask(); 
executorService.submit(task); 
executorService.submit(call);  
// =====================================================  
// Task或是Call中可以读取,值是"value-set-in-parent" 
String value = context.get();

3 ### Utilizar Java Agent  para modificar JDK clase de implementación del grupo de hilos

// ## 1. 框架上层逻辑,后续流程框架调用业务 ## 
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); 
context.set("value-set-in-parent");  
// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ## 
ExecutorService executorService = Executors.newFixedThreadPool(3);  
Runnable task = new RunnableTask(); 
Callable call = new CallableTask(); 
executorService.submit(task); 
executorService.submit(call);  
// ## 3. 框架下层逻辑 ## 
// Task或是Call中可以读取,值是"value-set-in-parent" 
String value = context.get();

La ventaja de usar un agente Java es que no requiere intervenir en el código de la aplicación. En cuanto a las desventajas, creo que el mayor problema es que, si olvidas añadir los parámetros al iniciar la JVM, podrías provocar un incidente.

Veamos brevemente la lógica del código fuente
3.png

4.png 

Respecto al problema B, normalmente no se tiene en cuenta en el desarrollo de aplicaciones, porque la sensibilidad a la eficiencia temporal no es tan alta. Sin embargo, en el desarrollo de algunos componentes de middleware de bajo nivel, puede darse bastante importancia a la optimización del rendimiento. Por ejemplo, netty optó por implementar por su cuenta FastThreadLocal Sustituye al ThreadLocal del jdk y, al mismo tiempo, proporciona el correspondiente FastThreadLocalThread,en FastThreadLocalThread y FastThreadLocal Cuando se utilizan conjuntamente, siempre se puede garantizar una complejidad temporal de búsqueda O(1). El principio de esta solución consiste en intercambiar espacio por tiempo: para evitar que las colisiones de hash reduzcan la eficiencia de las consultas en la tabla hash, se genera un array lo suficientemente largo, y cada FastThreadLocal Durante la construcción se asigna a cada uno un índice único de matriz, como si cada persona tuviera su propio sitio; al consultar, basta con utilizar el índice propio para obtener directamente el elemento de la matriz.
Aquí dejo brevemente el código fuente
5.png


Sección de comentarios

Enviando

Elige tu tema

El tema se aplica a todo el sitio.