Vai al contenuto principale

Cerca su CooLWJ

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

Lingua

Contesto dei thread Java

ThreadLocal è una classe di utilità piuttosto comune del pacchetto java.lang del JDK, che implementa la funzionalità di contesto del thread. L'applicazione più frequente è probabilmente l'uso di ThreadLocal per conservare informazioni essenziali, come il contesto dell'utente dopo l'accesso.

Il contesto del thread può anche risolvere con eleganza molti problemi apparentemente complessi, come quello dell'integrazione dei framework spring-mybatis:
Quando sviluppiamo nell'ecosistema spring, in genere affidiamo a spring la gestione delle transazioni, usando l'annotazione transactional per configurarne l'attivazione, il livello di isolamento e altre informazioni. Inoltre, non accediamo al database direttamente tramite jdbc, ma attraverso il framework mybatis. spring e mybatis sono due framework completamente indipendenti, eppure quando li usiamo sembra che la connection al database aperta da spring possa essere ottenuta e utilizzata direttamente da mybatis. Praticamente non dobbiamo fare nulla: basta aggiungere il pacchetto jar mybatis-spring-boot-stater.
Il punto chiave che rende possibile questa elegante soluzione è ThreadLocal. In linea generale, il pacchetto JAR stater finirà per introdurre per noi un MybatisAutoConfiguration e iniettarlo tramite SPI nel contenitore dei bean del nostro servizio, in MybatisAutoConfiguration è stato configurato in mybatis necessario SqlSessionFactory,e vi inietta il proprio di Spring SpringManagedTransactionFactory(che implementa TransactionFactory,la factory delle transazioni è un meccanismo di estensione fornito da mybatis, che permette di delegare la gestione delle transazioni)。
Il flusso complessivo di esecuzione prevede che il metodo con l'annotazione transactional venga infine gestito da un proxy AOP; la classe proxy ottiene una connection dal dataSource nell'advice eseguito prima del metodo e la inserisce infine in un ThreadLocal (in realtà nel ThreadLocal viene inserita una map, con dataSource come chiave e connectionHolder come valore, equivalente alla connection); successivamente, quando mybatis esegue SQL, passa prima attraverso TransactionFactory Ottenere la connection, perché a questo punto ciò che è stato iniettato TransactionFactory Sì SpringManagedTransactionFactory,quindi recupera da ThreadLocal la connection del dataSource corrispondente.

Breve introduzione al principio di funzionamento di ThreadLocal:

tapd_50394669_1660562598_27.png

ThreadLocalMap è una tabella hash che risolve le collisioni tramite scansione lineare; ogni thread ne genera una e ogni oggetto ThreadLocal contiene un threadLocalHashCode Serve a calcolare la chiave hash.
Facciamo un confronto con HashMap,ThreadLocalMap La scelta del sondaggio lineare come algoritmo di risoluzione delle collisioni, invece del concatenamento, potrebbe basarsi sulle seguenti considerazioni:
1、Considerazioni sulla memoria: poiché i contesti d'uso sono diversi, la quantità di dati in ThreadLocalMap è generalmente molto inferiore a quella di HashMap. Con pochi dati, il metodo del concatenamento comporta un consumo di spazio aggiuntivo considerevole per i puntatori
2、Efficienza: sulla base di 1, quando gli elementi sono pochi, lo spazio risparmiato può essere usato per generare una tabella hash con un fattore di carico inferiore, riducendo le collisioni e migliorando la velocità media di ricerca

Diagramma delle relazioni tra ThreadLocal, thread e ThreadLocalMap

Problema
A Negli scenari con attività basate su pool di thread, esiste un problema di propagazione del contesto, soprattutto nel modello di programmazione web basato su Tomcat. Poiché Tomcat stesso usa un pool di thread per gestire gli scambi di richieste e risposte tra server e client, ogni applicazione web deve gestire manualmente la corretta propagazione del contesto dei thread.
B Problema di efficienza: se nell'applicazione ci sono troppi ThreadLocal, l'espansione della tabella hash rallenta la ricerca

Per il problema A, le soluzioni comuni sono piuttosto uniformi; riporto direttamente il codice del nostro sistema
2.png
Questa soluzione presenta due problemi:
1、Se in seguito si aggiungono nuovi ThreadLocal che devono essere trasmessi correttamente, occorre modificare il codice aggiungendoli alla logica di wrap; una sola omissione può causare un bug
2、In alcuni scenari, il compito può essere eseguito dal thread stesso che lo ha sottoposto, per esempio:

  • Quando il pool di thread è pieno e il suo RejectedExecutionHandler si utilizza CallerRunsPolicy , i compiti inviati al pool di thread vengono eseguiti direttamente nel thread che li invia; in questo caso, la logica finale di pulizia provoca la perdita del contesto del thread chiamante

  • Analogamente, utilizzando ForkJoinPool (include l'esecuzione parallela Stream e CompletableFuture ,a livello sottostante utilizza ForkJoinPool )in questa situazione, ciò che si dispiega ForkJoinTask verrà eseguito direttamente nel thread che invia il task. Anche la pulizia finale causerà la perdita del contesto del thread di invio

Soluzione:
Problema 1:se la classe ThreadLocal offrisse un'API per sapere quanti oggetti ThreadLocal sono attualmente presenti nell'applicazione, il problema sarebbe risolto; purtroppo non la offre. Volendo forzare la mano, si può usare la reflection per accedere, nell'oggetto thread, a inheritableThreadLocals(si tratta proprio di un ThreadLocalMap), quindi basta scorrere la tabella hash ed estrarre tutte le entry per ottenere tutte le coppie chiave-valore di threadlocal. Un altro approccio consiste nell’aggiungere un ulteriore livello di incapsulamento a ThreadLocal del JDK, integrando le funzionalità descritte sopra.
Problema 2:si può risolvere perfezionando la logica di wrap:all'avvio dell'esecuzione del thread, si acquisisce e si conserva temporaneamente il suo contesto corrente, quindi lo si sostituisce con quello del thread che ha inviato il compito; prima della fine dell'esecuzione, invece di cancellarlo, si ripristina il contesto originale usando i dati conservati temporaneamente.

transmittable-thread-local
Esiste già una soluzione pronta: il suo funzionamento si basa sui due approcci descritti sopra, e a questo processo è stato dato un nome  CRR(capture/replay/restore)  modalità。
Ci sono tre modalità d'uso:
1 ### Modifica Runnable e 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 Decorare il pool di thread

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 ### Utilizzo Java Agent per modificare JDK Classe di implementazione del pool di thread

// ## 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();

Il vantaggio di usare un Java agent è che non richiede interventi sul codice dell'applicazione; quanto agli svantaggi, ritengo che il problema maggiore sia che, se si dimenticano i parametri all'avvio della JVM, si rischia un incidente。

Diamo una rapida occhiata alla logica del codice sorgente
3.png

4.png 

Quanto al problema B, in realtà nello sviluppo delle applicazioni in genere non lo si prende in considerazione, perché la sensibilità all'efficienza temporale non è così elevata. Nello sviluppo di alcuni middleware di basso livello, invece, l'ottimizzazione delle prestazioni può avere maggiore importanza. Per esempio, netty ha scelto di implementare autonomamente FastThreadLocal Sostituisce ThreadLocal del JDK e fornisce anche il relativo FastThreadLocalThread,a FastThreadLocalThread e FastThreadLocal Usati insieme, possono garantire sempre una complessità temporale di ricerca O(1). Il principio di questa soluzione è scambiare spazio con tempo: per evitare che le collisioni hash riducano l'efficienza delle ricerche nella tabella hash, viene generato un array sufficientemente lungo, ogni FastThreadLocal Durante la costruzione viene assegnato un indice univoco dell'array, come se ognuno avesse il proprio posto riservato; durante la ricerca basta usare il proprio index per recuperare direttamente il valore dall'array。
Riporto brevemente il codice sorgente
5.png


Commenti

Invio in corso

Scegli il tema

Il tema si applica a tutto il sito.