Eine Einführung in ThreadLocal in Java

1. Übersicht

In diesem Artikel betrachten wir das ThreadLocal- Konstrukt aus dem Paket java.lang . Dies gibt uns die Möglichkeit, Daten für den aktuellen Thread einzeln zu speichern - und sie einfach in einen speziellen Objekttyp zu verpacken.

2. ThreadLocal- API

Das TheadLocal Konstrukt ermöglicht es uns , um Daten zu speichern , die sein wird , zugänglich nur durch Thread eines bestimmten .

Angenommen, wir möchten einen Integer- Wert haben, der mit dem spezifischen Thread gebündelt wird:

ThreadLocal threadLocalValue = new ThreadLocal();

Wenn wir diesen Wert aus einem Thread verwenden möchten, müssen wir nur eine get () - oder set () -Methode aufrufen . Einfach ausgedrückt können wir denken, dass ThreadLocal Daten in einer Karte speichert - mit dem Thread als Schlüssel.

Aus diesem Grund erhalten wir beim Aufrufen einer get () -Methode für threadLocalValue einen Integer- Wert für den anfordernden Thread:

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

Wir können eine Instanz von ThreadLocal erstellen, indem wir die statische Methode withInitial () verwenden und einen Lieferanten an sie übergeben:

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

Um den Wert aus dem ThreadLocal zu entfernen , können wir die remove () -Methode aufrufen :

threadLocal.remove();

Um zu sehen, wie ThreadLocal richtig verwendet wird, sehen wir uns zunächst ein Beispiel an, in dem kein ThreadLocal verwendet wird. Anschließend schreiben wir unser Beispiel neu, um dieses Konstrukt zu nutzen.

3. Speichern von Benutzerdaten in einer Karte

Betrachten wir ein Programm, das die benutzerspezifischen Kontextdaten für eine bestimmte Benutzer-ID speichern muss :

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

Wir möchten einen Thread pro Benutzer-ID haben. Wir erstellen eine SharedMapWithUserContext- Klasse, die die Runnable- Schnittstelle implementiert . Die Implementierung in der run () -Methode ruft eine Datenbank über die UserRepository- Klasse auf, die ein Context- Objekt für eine bestimmte userId zurückgibt .

Als Nächstes speichern wir diesen Kontext in der ConcurentHashMap, die von userId eingegeben wird :

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 }

Wir können unseren Code einfach testen, indem wir zwei Threads für zwei verschiedene Benutzer-IDs erstellen und starten und bestätigen, dass wir zwei Einträge in der userContextPerUserId- Map haben:

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

4. Speichern von Benutzerdaten in ThreadLocal

Wir können unser Beispiel umschreiben den Benutzer zu speichern Context - Instanz mit einem Thread . Jeder Thread hat eine eigene ThreadLocal- Instanz.

Bei der Verwendung von ThreadLocal müssen wir sehr vorsichtig sein, da jede ThreadLocal- Instanz einem bestimmten Thread zugeordnet ist. In unserem Beispiel haben wir einen dedizierten Thread für jede bestimmte Benutzer-ID , und dieser Thread wird von uns erstellt, sodass wir die volle Kontrolle darüber haben.

Die run () -Methode ruft den Benutzerkontext ab und speichert ihn mit der set () -Methode in der ThreadLocal- Variablen :

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 }

Wir können es testen, indem wir zwei Threads starten, die die Aktion für eine bestimmte Benutzer-ID ausführen :

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

Nach dem Ausführen dieses Codes sehen wir in der Standardausgabe, dass ThreadLocal für einen bestimmten Thread festgelegt wurde:

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'}

Wir können sehen, dass jeder Benutzer seinen eigenen Kontext hat .

5. ThreadLocal s und Thread Pools

ThreadLocal bietet eine benutzerfreundliche API, um einige Werte auf jeden Thread zu beschränken. Dies ist ein vernünftiger Weg, um Thread-Sicherheit in Java zu erreichen. Allerdings sollten wir besonders vorsichtig sein , wenn wir mit Thread s und Thread - Pools zusammen.

Um die mögliche Einschränkung besser zu verstehen, betrachten wir das folgende Szenario:

  1. Zunächst leiht die Anwendung einen Thread aus dem Pool aus.
  2. Anschließend werden einige Thread-begrenzte Werte im ThreadLocal des aktuellen Threads gespeichert .
  3. Sobald die aktuelle Ausführung abgeschlossen ist, gibt die Anwendung den ausgeliehenen Thread an den Pool zurück.
  4. Nach einer Weile leiht sich die Anwendung denselben Thread aus, um eine weitere Anforderung zu verarbeiten.
  5. Da die Anwendung beim letzten Mal nicht die erforderlichen Bereinigungen durchgeführt hat, werden möglicherweise dieselben ThreadLocal- Daten für die neue Anforderung erneut verwendet.

Dies kann bei sehr gleichzeitigen Anwendungen überraschende Konsequenzen haben.

Eine Möglichkeit, dieses Problem zu lösen, besteht darin, jedes ThreadLocal manuell zu entfernen, sobald wir es nicht mehr verwenden. Da dieser Ansatz strenge Codeüberprüfungen erfordert, kann er fehleranfällig sein.

5.1. Die Ausweitung des ThreadPoolExecutor

Wie sich herausstellt, ist es möglich, die ThreadPoolExecutor- Klasse zu erweitern und eine benutzerdefinierte Hook-Implementierung für die Methoden beforeExecute () und afterExecute () bereitzustellen . Der Thread-Pool ruft die beforeExecute () -Methode auf, bevor etwas mit dem ausgeliehenen Thread ausgeführt wird. Andererseits ruft es die afterExecute () -Methode auf, nachdem unsere Logik ausgeführt wurde.

Daher können wir die ThreadPoolExecutor- Klasse erweitern und die ThreadLocal- Daten in der afterExecute () -Methode entfernen :

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

Wenn wir unsere Anforderungen an diese Implementierung von ExecutorService senden , können wir sicher sein, dass die Verwendung von ThreadLocal und Thread-Pools keine Sicherheitsrisiken für unsere Anwendung mit sich bringt .

6. Fazit

In diesem kurzen Artikel haben wir uns das ThreadLocal- Konstrukt angesehen. Wir haben die Logik implementiert, die ConcurrentHashMap verwendet , die von Threads gemeinsam genutzt wurde, um den einer bestimmten Benutzer-ID zugeordneten Kontext zu speichern . Als Nächstes haben wir unser Beispiel neu geschrieben, um ThreadLocal zum Speichern von Daten zu nutzen, die einer bestimmten Benutzer-ID und einem bestimmten Thread zugeordnet sind.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie auf GitHub.