Un'introduzione a ThreadLocal in Java

1. Panoramica

In questo articolo, esamineremo il costrutto ThreadLocal dal pacchetto java.lang . Questo ci dà la possibilità di memorizzare i dati individualmente per il thread corrente e semplicemente avvolgerli in un tipo speciale di oggetto.

2. ThreadLocal API

Il costrutto TheadLocal ci consente di memorizzare dati che saranno accessibili solo da un thread specifico .

Diciamo che vogliamo avere un valore Integer che verrà fornito in bundle con il thread specifico:

ThreadLocal threadLocalValue = new ThreadLocal();

Successivamente, quando vogliamo utilizzare questo valore da un thread, dobbiamo solo chiamare un metodo get () o set () . In poche parole, possiamo pensare che ThreadLocal memorizzi i dati all'interno di una mappa, con il thread come chiave.

Per questo motivo, quando chiamiamo un metodo get () su threadLocalValue , otterremo un valore Integer per il thread richiedente:

threadLocalValue.set(1); Integer result = threadLocalValue.get();

Possiamo costruire un'istanza di ThreadLocal utilizzando il metodo statico withInitial () e passandovi un fornitore:

ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 1);

Per rimuovere il valore da ThreadLocal , possiamo chiamare il metodo remove () :

threadLocal.remove();

Per vedere come utilizzare correttamente ThreadLocal , in primo luogo, esamineremo un esempio che non utilizza ThreadLocal , quindi riscriveremo il nostro esempio per sfruttare quel costrutto.

3. Memorizzazione dei dati utente in una mappa

Consideriamo un programma che deve memorizzare i dati di contesto specifici dell'utente per un dato ID utente:

public class Context { private String userName; public Context(String userName) { this.userName = userName; } }

Vogliamo avere un thread per ID utente. Creeremo una classe SharedMapWithUserContext che implementa l' interfaccia Runnable . L'implementazione nel metodo run () chiama un database tramite la classe UserRepository che restituisce un oggetto Context per un dato userId .

Successivamente, memorizziamo quel contesto in ConcurentHashMap con chiave userId :

public class SharedMapWithUserContext implements Runnable { public static Map userContextPerUserId = new ConcurrentHashMap(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContextPerUserId.put(userId, new Context(userName)); } // standard constructor }

Possiamo facilmente testare il nostro codice creando e avviando due thread per due diversi userId e affermando di avere due voci nella mappa userContextPerUserId :

SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1); SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start(); assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);

4. Memorizzazione dei dati utente in ThreadLocal

Possiamo riscrivere il nostro esempio per memorizzare l' istanza Context dell'utente usando un ThreadLocal . Ogni thread avrà la propria istanza ThreadLocal .

Quando si utilizza ThreadLocal , è necessario prestare molta attenzione perché ogni istanza di ThreadLocal è associata a un thread particolare. Nel nostro esempio, abbiamo un thread dedicato per ogni particolare ID utente e questo thread viene creato da noi, quindi abbiamo il pieno controllo su di esso.

Il metodo run () recupererà il contesto utente e lo memorizzerà nella variabile ThreadLocal utilizzando il metodo set () :

public class ThreadLocalWithUserContext implements Runnable { private static ThreadLocal userContext = new ThreadLocal(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContext.set(new Context(userName)); System.out.println("thread context for given userId: " + userId + " is: " + userContext.get()); } // standard constructor }

Possiamo testarlo avviando due thread che eseguiranno l'azione per un dato userId :

ThreadLocalWithUserContext firstUser = new ThreadLocalWithUserContext(1); ThreadLocalWithUserContext secondUser = new ThreadLocalWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start();

Dopo aver eseguito questo codice vedremo sull'output standard che ThreadLocal è stato impostato per thread dato:

thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'} thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}

Possiamo vedere che ciascuno degli utenti ha il proprio contesto .

5. ThreadLocal se pool di thread

ThreadLocal fornisce un'API di facile utilizzo per limitare alcuni valori a ciascun thread. Questo è un modo ragionevole per ottenere la sicurezza dei thread in Java. Tuttavia, dovremmo prestare la massima attenzione quando usiamo ThreadLocal e pool di thread insieme.

Per comprendere meglio il possibile avvertimento, consideriamo il seguente scenario:

  1. Innanzitutto, l'applicazione prende in prestito un thread dal pool.
  2. Quindi memorizza alcuni valori limitati al thread nel ThreadLocal del thread corrente .
  3. Al termine dell'esecuzione corrente, l'applicazione restituisce il thread preso in prestito al pool.
  4. Dopo un po ', l'applicazione prende in prestito lo stesso thread per elaborare un'altra richiesta.
  5. Poiché l'ultima volta l'applicazione non ha eseguito le pulizie necessarie, potrebbe riutilizzare gli stessi dati ThreadLocal per la nuova richiesta.

Ciò può causare conseguenze sorprendenti in applicazioni altamente concorrenti.

Un modo per risolvere questo problema è rimuovere manualmente ogni ThreadLocal una volta che abbiamo finito di usarlo. Poiché questo approccio richiede rigorose revisioni del codice, può essere soggetto a errori.

5.1. Estensione di ThreadPoolExecutor

A quanto pare, è possibile estendere la classe ThreadPoolExecutor e fornire un'implementazione hook personalizzata per i metodi beforeExecute () e afterExecute () . Il pool di thread chiamerà il metodo beforeExecute () prima di eseguire qualsiasi cosa utilizzando il thread preso in prestito. D'altra parte, chiamerà il metodo afterExecute () dopo aver eseguito la nostra logica.

Pertanto, possiamo estendere la classe ThreadPoolExecutor e rimuovere i dati ThreadLocal nel metodo afterExecute () :

public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor { @Override protected void afterExecute(Runnable r, Throwable t) { // Call remove on each ThreadLocal } }

Se inviamo le nostre richieste a questa implementazione di ExecutorService , possiamo essere certi che l'utilizzo di ThreadLocal e dei pool di thread non introdurrà rischi per la sicurezza per la nostra applicazione.

6. Conclusione

In questo breve articolo, abbiamo esaminato il costrutto ThreadLocal . Abbiamo implementato la logica che utilizza ConcurrentHashMap condivisa tra i thread per archiviare il contesto associato a un particolare ID utente. Successivamente, abbiamo riscritto il nostro esempio per sfruttare ThreadLocal per archiviare i dati associati a un particolare ID utente e a un particolare thread.

L'implementazione di tutti questi esempi e frammenti di codice può essere trovata su GitHub.