Eingehende Analyse von ThreadLocal-Nutzungsszenarien, Implementierungsprinzipien und Designideen

Vorwort

ThreadLocal kann zum Speichern lokaler Daten von Threads verwendet werden, um Thread-Daten zu isolieren.

Eine unsachgemäße Verwendung von ThreadLocal kann zu Speicherlecks führen. Die Fehlerbehebung bei Speicherlecks erfordert nicht nur Vertrautheit mit der JVM und den guten Einsatz verschiedener Analysetools, sondern auch mühsame Arbeit.

Wenn Sie das Prinzip verstehen und es richtig anwenden, wird es keine Unfälle verursachen.

In diesem Artikel wird ThreadLocal unter den Gesichtspunkten von Nutzungsszenarien, Implementierungsprinzipien, Speicherverlusten, Designideen usw. analysiert und auch über InheritableThreadLocal gesprochen.

ThreadLocal-Nutzungsszenarien

Was ist Kontext?

Wenn beispielsweise ein Thread eine Anfrage verarbeitet, durchläuft die Anfrage den MVC-Prozess. Da der Prozess sehr lang ist, werden viele Methoden durchlaufen. Diese Methoden können als Kontexte bezeichnet werden.

ThreadLocal wird verwendet, um häufig verwendete Daten, Sitzungsinformationen, lokale Thread-Variablen usw. im Kontext zu speichern.

Verwenden Sie beispielsweise einen Interceptor, um vor der Verarbeitung der Anforderung die Informationen des angemeldeten Benutzers über das Token in der Anforderung abzurufen, und speichern Sie die Benutzerinformationen in ThreadLocal, sodass die Benutzerinformationen bei der nachfolgenden Verarbeitung der Anforderung direkt von ThreadLocal abgerufen werden können.

Wenn der Thread wiederverwendet wird, sollten die Daten nach der Verwendung (nach der Verarbeitung durch den Interceptor) gelöscht werden, um Datenverwechslungen zu vermeiden.

Häufig verwendete Methoden von ThreadLocal sind: set(), entsprechend der Speicherung, Erfassung und Löschung.get()remove()

ThreadLocal kann zur einfacheren Verwendung in einer Toolklasse platziert werden

public class ContextUtils {
    public static final ThreadLocal<UserInfo> USER_INFO_THREAD_LOCAL = new ThreadLocal();
}

Interceptor-Pseudocode

//执行前 存储
public boolean postHandle(HttpServletRequest request)  {
    //解析token获取用户信息
	String token = request.getHeader("token");
	UserInfo userInfo = parseToken(token);   
	//存入
	ContextUtils.USER_INFO_THREAD_LOCAL.set(userInfo);
    
    return true;
}


//执行后 删除
public void postHandle(HttpServletRequest request)  {
    ContextUtils.USER_INFO_THREAD_LOCAL.remove();
}

wenn man es benutzt

//提交订单
public void orderSubmit(){
    //获取用户信息
    UserInfo userInfo = ContextUtils.USER_INFO_THREAD_LOCAL.get();
    //下单
    submit(userInfo);
    //删除购物车
    removeCard(userInfo);
}

Um ThreadLocal besser nutzen zu können, sollten wir seine Implementierungsprinzipien verstehen und Unfälle durch unsachgemäße Verwendung vermeiden.

ThreadLocalMap

Thread Es gibt zwei Felder im Thread, in denen die interne Klasse ThreadLocalMap von ThreadLocal gespeichert wird

public class Thread implements Runnable {    
    
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

threadLocals wird verwendet, um ThreadLocal zu implementieren

inheritableThreadLocals wird verwendet, um InheritableThreadLocal zu implementieren (inheritableThreadLocal wird später besprochen)

Bild.png

Die Implementierung von ThreadLocalMap ist eine Hash-Tabelle, und ihre interne Klasse Entry ist der Knoten der Hash-Tabelle. Die Hash-Tabelle ThreadLocalMap wird durch das Entry-Array implementiert.

public class ThreadLocal<T> {
    //,,,
	static class ThreadLocalMap {
        //...
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
	}
}

Der Schlüssel in der Knotenkonstruktion ist ThreadLocal und der Wert ist der Wert, der gespeichert werden muss.

Gleichzeitig erbt der Knoten schwache Referenzen. Durch Generika und Konstruktoren können Sie erkennen, dass ThreadLocal auf eine schwache Referenz gesetzt wird.

Schüler, die schwache Referenzen nicht verstehen, können diesen Artikel lesen: Eine ausführliche Einführung in JVM (14) Speicherüberläufe, Lecks und Referenzen)

Bild.png

Satz

In der Methode der Datenspeicherung

Bild.png

Holen Sie sich ThreadLocalMap. Wenn nicht, initialisieren Sie ThreadLocalMap (verzögertes Laden).

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
   
    if (map != null) {
        //添加数据
        map.set(this, value);
    } else {
        //没有就初始化
        createMap(t, value);
    }
}

createMap

Erstellen Sie eine ThreadLocalMap und weisen Sie sie den threadLocals des aktuellen Threads zu.

	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

Erstellen Sie eine ThreadLocalMap und initialisieren Sie ein Array mit einer Länge von 16

	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化数组 16
        table = new Entry[INITIAL_CAPACITY];
        //获取下标
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //构建节点
        table[i] = new Entry(firstKey, firstValue);
        //设置大小
        size = 1;
        //设置负载因子
        setThreshold(INITIAL_CAPACITY);
   }

ThreadLocalMap.set

Erhalten Sie den Index durch Hashing. Wenn ein Hash-Konflikt auftritt, durchlaufen Sie die Hash-Tabelle (nicht mehr mit der Kettenadressmethode), bis an der Position kein Knoten mehr vorhanden ist, und erstellen Sie dann.

Wenn beim Durchlaufen ein Knoten vorhanden ist, wird der Schlüssel zum Vergleich entsprechend dem Knoten herausgenommen. Wenn dies der Fall ist, wird er überschrieben. Wenn der Knoten keinen Schlüssel hat, bedeutet dies, dass der ThreadLocal des Knotens vorhanden war recycelt (abgelaufen). Um Speicherlecks zu verhindern, wird der Knoten bereinigt.

Schließlich wird überprüft, ob an anderen Standorten abgelaufene Knoten vorhanden sind, diese bereinigt und auf Erweiterung geprüft.

private void set(ThreadLocal<?> key, Object value) {

    //获取哈希表
    Entry[] tab = table;
    int len = tab.length;
    //获取下标
    int i = key.threadLocalHashCode & (len-1);

    //遍历 直到下标上没有节点
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        //获取key
        ThreadLocal<?> k = e.get();
		//key如果存在则覆盖
        if (k == key) {
            e.value = value;
            return;
        }
		//如果key不存在 说明该ThreadLocal以及不再使用(GC回收),需要清理防止内存泄漏
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //构建节点
    tab[i] = new Entry(key, value);
    //计数
    int sz = ++size;
    //清理其他过期的槽,如果满足条件进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

Verwenden Sie beim Erhalten des Hash-Werts die atomare Klasse, die den Hash-Wert erhöht, und die Schrittgröße ist die Anzahl der Inkremente jedes Mal (möglicherweise nach Recherche und Tests, um Hash-Konflikte zu minimieren).

	//获取哈希值
    private final int threadLocalHashCode = nextHashCode();
    //哈希值自增器
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
    //增长步长
    private static final int HASH_INCREMENT = 0x61c88647;

    //获取哈希值
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

nextIndex soll den nächsten Index abrufen und auf 0 zurückkehren, wenn die Obergrenze überschritten wird.

		private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

erhalten

beim Abrufen von Daten

Bild.png

Rufen Sie die ThreadLocalMap des aktuellen Threads ab, initialisieren Sie sie, wenn sie leer ist, andernfalls rufen Sie den Knoten ab

	public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //获取节点
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //初始化(懒加载)
        return setInitialValue();
    }

Wenn Sie den Knoten erhalten, rufen Sie zuerst den Index basierend auf dem Hash-Wert ab, überprüfen Sie dann den Knoten und vergleichen Sie den Schlüssel. Wenn er nicht übereinstimmt, bedeutet dies, dass der Schlüssel abgelaufen ist und ein Speicherverlust auftreten kann. Sie müssen den Hash bereinigen Tisch.

		private Entry getEntry(ThreadLocal<?> key) {
            //获取下标
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //如果匹配 则返回
            if (e != null && e.get() == key)
                return e;
            else
                //匹配不到 去清理
                return getEntryAfterMiss(key, i, e);
        }

Speicherleck

Während des Festlegens und Abrufens von Daten beurteilen wir, ob der Schlüssel abgelaufen ist. Wenn er abläuft, bereinigen Sie ihn.

Tatsächlich kann die unsachgemäße Verwendung von ThreadLocal zu Speicherverlusten führen.

Um Speicherverluste durch unsachgemäße Verwendung zu vermeiden, versuchen Designer, diese abgelaufenen ThreadLocals mit gängigen Methoden zu bereinigen.

Wie bereits erwähnt, erben Knoten schwache Referenzen und legen den Schlüssel für schwache Referenzen in der Konstruktion fest (d. h. ThreadLocal).

Wenn ThreadLocal nirgendwo verwendet wird, setzt der nächste GC den Schlüssel des Knotens auf leer

Wenn der Wert nicht mehr verwendet wird, aber der Knoteneintrag (null, Wert) vorhanden ist, kann der Wert nicht recycelt werden, was zu einem Speicherverlust führt.

Bild.png

Versuchen Sie daher nach der Verwendung der Daten, sie mit „Remove“ zu löschen.

Darüber hinaus prüfen Designer Knoten mit leeren Schlüsseln und löschen sie mit gängigen Methoden wie Set, Get und Remove, um Speicherlecks zu vermeiden.

Design Thinking

Warum sollte der Schlüssel im Eintrag, also ThreadLocal, auf eine schwache Referenz gesetzt werden?

Stellen wir uns zunächst ein Szenario vor: Threads werden in unseren Diensten häufig wiederverwendet, und in einigen Szenarien wird ThreadLocal längere Zeit nicht verwendet.

Wenn sowohl der Schlüssel als auch der Wert des Knoteneintrags starke Referenzen sind und ThreadLocal nicht mehr verwendet wird, wird ThreadLocal immer noch als starke Referenz im Knoten gespeichert und kann nicht wiederverwendet werden, was einem Speicherverlust gleichkommt.

Nachdem ThreadLocal auf eine schwache Referenz gesetzt wurde, kommt es immer noch zu Speicherlecks, wenn der Wert in diesem Szenario nicht mehr verwendet wird. Daher werden in den Methoden set, get und remove Knoten mit leeren Schlüsseln überprüft und gelöscht, um Speicherlecks zu vermeiden.

Da der Wert möglicherweise nicht recycelt wird, warum legen Sie ihn nicht als schwache Referenz fest?

Da der Wert Thread-isolierte Daten speichert und der Wert auf eine schwache Referenz gesetzt ist und die äußere Schicht das dem Wert entsprechende Objekt nicht verwendet, hat er keine starke Referenz und führt beim nächsten GC-Recycling zu Datenverlust.

InheritableThreadLocal

InheritableThreadLocal erbt ThreadLocal und wird zum Übertragen von Thread-Variablen zwischen übergeordneten und untergeordneten Threads verwendet.

	public void testInheritableThreadLocal(){
        InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();

        itl.set("main");

        new Thread(()->{
            //main
            System.out.println(itl.get());
        }).start();
    }

Wie bereits erwähnt, wird eine andere ThreadLocalMap im Thread für InheritableThreadLocal verwendet.

Wenn beim Erstellen eines Threads inheritableThreadLocals im übergeordneten Thread nicht leer ist, wird es übergeben

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        //....
    
        //如果父线程中inheritableThreadLocals 不为空 则传递
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
    }

Zusammenfassen

ThreadLocal wird zum Isolieren von Daten zwischen Threads verwendet und kann Daten zur Verwendung im Kontext speichern. Da Threads möglicherweise wiederverwendet werden, müssen sie nach der Verwendung gelöscht werden, um Datenverwechslungen zu vermeiden.

ThreadLocalMap wird im Thread-Thread gespeichert. ThreadLocalMap ist eine Hash-Tabelle, die die offene Adressierungsmethode verwendet, um Hash-Konflikte zu lösen. Der Knotenspeicherschlüssel ist ThreadLocal und der Wert speichert die vom Thread zu speichernden Daten.

Der Knoten erbt die schwache Referenz und legt ThreadLocal als schwache Referenz fest. Dies führt dazu, dass ThreadLocal das nächste Mal recycelt wird, wenn der GC nicht mehr verwendet wird. Zu diesem Zeitpunkt ist der Schlüssel leer. Wenn der Wert nicht mehr verwendet wird, Wenn der Knoten jedoch nicht gelöscht wird, wird der Wert verwendet, was zu einem Speicherverlust führt

Gehen Sie bei den gängigen Methoden von ThreadLocal wie Set, Get, Remove usw. beim Durchlaufen des Arrays zurück und löschen Sie die abgelaufenen Knoten (der Schlüssel ist leer), um Speicherverluste zu vermeiden.

Wenn ThreadLocal auf eine starke Referenz eingestellt ist, treten Speicherlecks auf, wenn ThreadLocal nicht mehr verwendet wird. Wenn ThreadLocal auf eine schwache Referenz eingestellt ist, können zwar auch Speicherlecks auftreten, diese Daten können jedoch mit gängigen Methoden überprüft und bereinigt werden; wenn Wert Wird auf eine schwache Referenz gesetzt, kommt es zu Datenverlust, wenn die äußere Schicht den Wert nicht verwendet

InheritableThreadLocal erbt ThreadLocal und wird für die ThreadLocal-Datenübertragung zwischen übergeordneten und untergeordneten Threads verwendet.

Zum Schluss (tun Sie es nicht kostenlos, drücken Sie einfach dreimal hintereinander, um um Hilfe zu bitten ~)

Dieser Artikel ist in der Spalte „Vom Punkt zur Linie und von der Linie zur Oberfläche“ enthalten, um in einfachen Worten ein Java-Wissenssystem für gleichzeitige Programmierung aufzubauen . Interessierte Studenten können weiterhin aufmerksam sein.

Die Notizen und Fälle dieses Artikels wurden in gitee-StudyJava und github-StudyJava aufgenommen . Interessierte Studierende können weiterhin unter stat~ darauf achten

Falladresse:

Gitee-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

Github-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

Wenn Sie Fragen haben, können Sie diese im Kommentarbereich diskutieren. Wenn Sie Cai Cais Schreiben für gut halten, können Sie es liken, verfolgen und sammeln, um es zu unterstützen~

Folgen Sie Cai Cai und teilen Sie weitere nützliche Informationen, öffentliches Konto: Cai Cais private Back-End-Küche

Dieser Artikel wurde von OpenWrite veröffentlicht, einem Blog, der mehrere Artikel veröffentlicht !

Lei Jun: Die offizielle Version von Xiaomis neuem Betriebssystem ThePaper OS wurde verpackt. Ein Popup-Fenster auf der Lotterieseite der Gome App beleidigt seinen Gründer. Die US-Regierung beschränkt den Export von NVIDIA H800 GPU nach China. Die Xiaomi ThePaper OS-Schnittstelle ist offengelegt. Ein Master hat Scratch verwendet, um den RISC-V-Simulator zu reiben, und er wurde erfolgreich ausgeführt. Linux-Kernel RustDesk Remote Desktop 1.2.3 veröffentlicht, verbesserte Wayland-Unterstützung Nach dem Herausziehen des Logitech-USB-Empfängers stürzte der Linux-Kernel ab. DHH scharfe Überprüfung der „Verpackungstools“. ": Das Frontend muss überhaupt nicht erstellt werden (No Build) JetBrains startet Writerside, um technische Dokumentation zu erstellen. Tools für Node.js 21 offiziell veröffentlicht
{{o.name}}
{{m.name}}

Ich denke du magst

Origin my.oschina.net/u/6903207/blog/10114997
Empfohlen
Rangfolge