ThreadLocal ist eine häufig verwendete Hilfsklasse im Paket java.lang des JDK, die einen Thread-Kontext bereitstellt. Der wohl häufigste Anwendungsfall besteht darin, mit ThreadLocal wichtige Informationen wie den Benutzerkontext nach der Anmeldung zu speichern.
Mit dem Threadkontext lassen sich auch viele scheinbar komplizierte Probleme elegant lösen, etwa die Integration der Frameworks spring und mybatis:
Bei der Entwicklung im spring-Ökosystem überlassen wir die Transaktionsverwaltung gewöhnlich spring. Mit der transactional-Annotation konfigurieren wir die Aktivierung von Transaktionen sowie Angaben wie deren Isolationsstufe. Auch auf die Datenbank greifen wir nicht direkt über jdbc zu, sondern über das mybatis-Framework. spring und mybatis sind zwei völlig unabhängige Frameworks. Bei ihrer Verwendung scheint mybatis jedoch direkt auf die von spring geöffnete Datenbank-connection zugreifen zu können. Wir müssen praktisch nichts tun; es genügt, das JAR-Paket mybatis-spring-boot-stater einzubinden.
Der entscheidende Punkt bei der Umsetzung dieses eleganten Designs ist ThreadLocal. Das Grundprinzip besteht darin, dass das stater-JAR-Paket letztlich für uns ein MybatisAutoConfiguration und über SPI in den Bean-Container unseres Dienstes injiziert, in MybatisAutoConfiguration darin konfiguriert mybatis benötigt SqlSessionFactory,und injizierte Springs eigene SpringManagedTransactionFactory(implementiert TransactionFactory, die Transaktionsfabrik ist ein von mybatis bereitgestellter Erweiterungsmechanismus, mit dem sich die Transaktionsverwaltung delegieren lässt).
Der gesamte Ablauf sieht so aus: Eine mit transactional annotierte Methode wird letztlich durch einen AOP-Proxy vertreten. Die Proxyklasse holt in einem Before-Advice eine connection aus der dataSource und legt sie schließlich in einem ThreadLocal ab (tatsächlich enthält das ThreadLocal eine Map, deren Schlüssel die dataSource und deren Wert ein connectionHolder ist, der hier der connection entspricht). Wenn mybatis anschließend SQL ausführt, greift es zunächst über TransactionFactory connection abrufen, da das zu diesem Zeitpunkt injizierte TransactionFactory Ja SpringManagedTransactionFactory,deshalb wird die zur jeweiligen dataSource gehörende connection aus ThreadLocal abgerufen。
Kurze Einführung in die Funktionsweise von ThreadLocal:
ThreadLocalMap ist eine Hashtabelle, die Kollisionen durch lineares Sondieren auflöst. Für jeden Thread wird eine eigene angelegt, und jedes ThreadLocal-Objekt enthält ein threadLocalHashCode Wird zur Berechnung des Hash-Schlüssels verwendet.
Zum Vergleich mit HashMap,ThreadLocalMap Die Wahl der linearen Sondierung anstelle der Verkettung zur Kollisionsbehandlung könnte auf folgenden Überlegungen beruhen:
1、Speicherbedarf: Aufgrund der unterschiedlichen Einsatzszenarien enthält eine ThreadLocalMap gewöhnlich wesentlich weniger Daten als eine HashMap. Bei kleinen Datenmengen verursacht die Verkettung einen erheblichen zusätzlichen Speicherbedarf für Zeiger
2、Effizienz: Aufbauend auf 1 kann bei kleinen Mengen der eingesparte Speicherplatz zur Erstellung einer Hashtabelle mit geringerem Belegungsfaktor genutzt werden, um Kollisionen zu verringern und die durchschnittliche Suchgeschwindigkeit zu erhöhen

Problem
A Bei Aufgaben, die auf Thread-Pools beruhen, treten Probleme mit der Kontextweitergabe auf. Dies gilt insbesondere für das Tomcat-basierte Web-Programmiermodell: Da Tomcat selbst einen Thread-Pool verwendet, um die Anfrage-Antwort-Interaktion zwischen Server und Client abzuwickeln, muss jede Webanwendung die korrekte Weitergabe des Thread-Kontexts manuell sicherstellen.
B Effizienzproblem:Wenn eine Anwendung zu viele ThreadLocal-Instanzen verwendet und dadurch die Hashtabelle anwächst, sinkt die Suchgeschwindigkeit
Für Problem A sind die gängigen Lösungsansätze recht einheitlich. Ich füge hier direkt den Code aus unserem System ein
Bei diesem Ansatz gibt es zwei Probleme:
1、Wenn später weitere ThreadLocal-Variablen korrekt übertragen werden müssen, muss der Code angepasst und die neue ThreadLocal-Variable in die wrap-Logik aufgenommen werden. Wird eine übersehen, entsteht ein Fehler
2、In manchen Situationen wird eine Aufgabe möglicherweise vom einreichenden Thread selbst ausgeführt, zum Beispiel:
Wenn der Threadpool voll ist und die zum Threadpool gehörende
RejectedExecutionHandlerVerwendet wirdCallerRunsPolicywerden die an den Threadpool übergebenen Aufgaben direkt im übergebenden Thread ausgeführt. Die abschließende Bereinigungslogik führt dann dazu, dass der Kontext dieses Threads verloren gehtAuf ähnliche Weise unter Verwendung von
ForkJoinPool(einschließlich paralleler AusführungStreamundCompletableFuture,verwendet internForkJoinPool) zum Einsatz kommt, wird das entfalteteForkJoinTaskwird direkt im Thread ausgeführt, der die Aufgabe einreicht. Das abschließende Bereinigen führt ebenfalls zum Verlust des Kontexts dieses Threads
Lösung:
Problem 1:Wenn die Klasse ThreadLocal eine API unterstützen würde, mit der sich die aktuelle Gesamtzahl der ThreadLocal-Objekte in der Anwendung ermitteln ließe, wäre das Problem leicht gelöst. Leider unterstützt sie das nicht. Will man es dennoch erzwingen, kann man per Reflection auf das folgende Attribut des thread-Objekts zugreifen: inheritableThreadLocals(also ein ThreadLocalMap)zugreifen und dann die Hashtabelle durchlaufen, um sämtliche entry-Elemente und damit alle threadlocal-Schlüssel-Wert-Paare abzurufen. Ein anderer Ansatz besteht darin, ThreadLocal aus dem JDK nochmals zu kapseln und um die oben genannten Funktionen zu ergänzen。
Problem 2: Es lässt sich durch eine Verbesserung der wrap-Logik lösen: Beim Start der Thread-Ausführung wird zunächst der Kontext des aktuellen Threads abgerufen und zwischengespeichert. Anschließend wird der Kontext dieses Threads durch den Kontext des einreichenden Threads ersetzt. Vor dem Ende der Ausführung wird der Kontext nicht gelöscht, sondern anhand der zwischengespeicherten Daten wiederhergestellt.
transmittable-thread-local
Eine fertige Lösung gibt es bereits. Ihr Implementierungsprinzip setzt bei den beiden oben genannten Ansätzen an, und dieser Ablauf hat auch einen Namen erhalten CRR(capture/replay/restore) Modus。
Es gibt drei Verwendungsmöglichkeiten:
1 ### Modifizierung Runnable und 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 Threadpool dekorieren
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 ### Verwendung Java Agent zur Modifizierung JDK Implementierungsklasse des Threadpools
// ## 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();Der Vorteil des Java-Agent-Ansatzes ist, dass er keine Eingriffe in den Anwendungscode erfordert. Der größte Nachteil ist meiner Ansicht nach, dass es zu einem Zwischenfall kommen kann, wenn man beim Start der JVM vergisst, die Parameter hinzuzufügen.
Ein kurzer Blick auf die Logik des Quellcodes
Was Problem B betrifft, wird es bei der Anwendungsentwicklung im Allgemeinen nicht berücksichtigt, da die Empfindlichkeit gegenüber der Zeiteffizienz nicht so hoch ist. Bei der Entwicklung mancher grundlegender Middleware kann die Leistungsoptimierung jedoch stärker im Vordergrund stehen. Beispielsweise hat sich netty dafür entschieden, selbst zu implementieren FastThreadLocal ersetzt ThreadLocal des JDK und stellt zugleich das dazugehörige FastThreadLocalThread,in FastThreadLocalThread und FastThreadLocal Bei kombinierter Verwendung lässt sich stets eine Suchzeitkomplexität von O(1) gewährleisten. Das Prinzip dieser Lösung besteht darin, Speicherplatz gegen Zeit einzutauschen. Damit Hashkollisionen in der Hashtabelle die Abfrageeffizienz nicht verringern, wird ein ausreichend langes Array erzeugt, jedes FastThreadLocal Bei der Erstellung wird jeweils ein eindeutiger Array-Index zugewiesen, sozusagen ein eigener Platz für jeden. Beim Abfragen kann man mit seinem eigenen index direkt auf den Eintrag im Array zugreifen.
Hier kurz der Quellcode