Analyse approfondie des scénarios d'utilisation de ThreadLocal, des principes de mise en œuvre et des idées de conception

Préface

ThreadLocal peut être utilisé pour stocker les données locales des threads afin d'isoler les données des threads.

Une mauvaise utilisation de ThreadLocal peut entraîner des fuites de mémoire. Le dépannage des fuites de mémoire nécessite non seulement une familiarité avec la JVM, une bonne utilisation de divers outils d'analyse, mais également un travail laborieux.

Si vous pouvez comprendre son principe et l'utiliser correctement, cela ne provoquera pas divers accidents.

Cet article analysera ThreadLocal sous les aspects des scénarios d'utilisation, des principes de mise en œuvre, des fuites de mémoire, des idées de conception, etc., et parlera également d'InheritableThreadLocal.

Scénarios d'utilisation de ThreadLocal

Qu’est-ce que le contexte ?

Par exemple, lorsqu'un thread traite une requête, la requête passera par le processus MVC. Comme le processus est très long, il passera par de nombreuses méthodes. Ces méthodes peuvent être appelées contextes.

ThreadLocal est utilisé pour stocker les données couramment utilisées, stocker les informations de session, stocker les variables locales du thread, etc.

Par exemple, utilisez un intercepteur pour obtenir les informations de l'utilisateur connecté via le jeton dans la demande avant de traiter la demande, et stockez les informations de l'utilisateur dans ThreadLocal, afin que les informations de l'utilisateur puissent être obtenues directement à partir de ThreadLocal lors du traitement ultérieur de la demande.

Si le fil est réutilisé, afin d'éviter toute confusion de données, les données doivent être supprimées après utilisation (après traitement par l'intercepteur).

Les méthodes couramment utilisées de ThreadLocal sont : set(), get(), remove()correspondant respectivement au stockage, à l'acquisition et à la suppression.

ThreadLocal peut être placé dans une classe d'outils pour une utilisation facile

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

Pseudocode de l'intercepteur

//执行前 存储
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();
}

en l'utilisant

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

Afin de mieux utiliser ThreadLocal, nous devons comprendre ses principes de mise en œuvre et éviter les accidents causés par une mauvaise utilisation.

DiscussionLocalMap

Thread Il y a deux champs dans le thread qui stockent la classe interne de ThreadLocal ThreadLocalMap

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

threadLocals est utilisé pour implémenter ThreadLocal

HeitableThreadLocals est utilisé pour implémenter InheritableThreadLocal (le ThreadLocal hérité sera discuté plus tard)

image.png

L'implémentation de ThreadLocalMap est une table de hachage, et sa classe interne Entry est le nœud de la table de hachage. La table de hachage ThreadLocalMap est implémentée par le tableau Entry.

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

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

La clé dans la construction du nœud est ThreadLocal et la valeur est la valeur qui doit être stockée.

En même temps, le nœud hérite de références faibles. Grâce aux génériques et aux constructeurs, vous pouvez savoir qu'il définit ThreadLocal sur une référence faible.

Les étudiants qui ne comprennent pas les références faibles peuvent consulter cet article : Une introduction approfondie à JVM (14) Débordements de mémoire, fuites et références)

image.png

ensemble

Dans la méthode de stockage des données

image.png

Récupérez ThreadLocalMap, sinon, initialisez ThreadLocalMap (chargement paresseux)

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);
    }
}

créer une carte

Créez un ThreadLocalMap et attribuez-le aux threadLocals du thread actuel.

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

Créez un ThreadLocalMap et initialisez un tableau d'une longueur de 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

Obtenez l'indice par hachage. Lorsqu'un conflit de hachage se produit, parcourez la table de hachage (n'utilisez plus la méthode d'adresse en chaîne) jusqu'à ce qu'il n'y ait plus de nœud à la position, puis construisez.

S'il y a un nœud pendant le parcours, la clé sera extraite en fonction du nœud pour comparaison. Si c'est le cas, elle sera écrasée. Si le nœud n'a pas de clé, cela signifie que le ThreadLocal du nœud a été recyclé (expiré). Afin d’éviter les fuites de mémoire, le nœud sera nettoyé.

Enfin, il vérifiera s'il y a des nœuds expirés dans d'autres emplacements, les nettoiera et vérifiera l'expansion.

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();
}

Lors de l'obtention de la valeur de hachage, utilisez la classe atomique qui incrémente la valeur de hachage, et la taille du pas correspond au nombre d'incréments à chaque fois (peut-être après des recherches et des tests, pour minimiser les conflits de hachage)

	//获取哈希值
    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 doit obtenir l'index suivant et revient à 0 lorsque la limite supérieure est dépassée.

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

obtenir

lors de l'obtention de données

image.png

Récupère le ThreadLocalMap du thread courant, initialise-le s'il est vide, sinon récupère le nœud

	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();
    }

Lors de l'obtention du nœud, obtenez d'abord l'indice basé sur la valeur de hachage, puis vérifiez le nœud et comparez la clé ; si cela ne correspond pas, cela signifie que la clé a expiré et une fuite de mémoire peut se produire. Vous devez nettoyer le hachage tableau.

		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);
        }

fuite de mémoire

Pendant le processus de configuration et d'obtention des données, nous jugerons si la clé a expiré. Si elle expire, nettoyez-la.

En fait, une mauvaise utilisation de ThreadLocal peut provoquer des fuites de mémoire.

Afin d'éviter les fuites de mémoire causées par une utilisation inappropriée, les concepteurs tentent de nettoyer ces ThreadLocals expirés à l'aide de méthodes courantes.

Comme mentionné précédemment, les nœuds héritent des références faibles et définissent la clé des références faibles dans la construction (c'est-à-dire ThreadLocal).

Lorsque ThreadLocal n'est utilisé nulle part, le prochain GC définira la clé du nœud sur vide

Si la valeur n'est plus utilisée, mais parce que le nœud Entry (null, value) existe, la valeur ne peut pas être recyclée, ce qui entraîne une fuite mémoire.

image.png

Par conséquent, après avoir utilisé les données, essayez d'utiliser Remove pour les supprimer.

De plus, les concepteurs vérifieront les nœuds avec des clés vides et les supprimeront à l'aide de méthodes courantes telles que set, get et delete pour éviter les fuites de mémoire.

La pensée de conception

Pourquoi la clé de l'entrée, c'est-à-dire ThreadLocal, devrait-elle être définie sur une référence faible ?

Imaginons d'abord un scénario : les threads sont souvent réutilisés dans nos services, et dans certains scénarios, ThreadLocal n'est pas utilisé pendant longtemps.

Si la clé et la valeur de l'entrée du nœud sont toutes deux des références fortes, une fois que le ThreadLocal n'est plus utilisé, le ThreadLocal est toujours stocké dans le nœud en tant que référence forte et ne peut pas être recyclé, ce qui équivaut à une fuite de mémoire.

Après avoir défini ThreadLocal sur une référence faible, des fuites de mémoire se produiront toujours si la valeur n'est plus utilisée dans ce scénario. Par conséquent, dans les méthodes set, get et Remove, les nœuds avec des clés vides seront vérifiés et supprimés pour éviter les fuites de mémoire.

Puisque la valeur ne peut pas être recyclée, pourquoi ne pas définir la valeur comme référence faible ?

Étant donné que value stocke des données isolées par thread, si value est définie sur une référence faible, lorsque la couche externe n'utilise pas l'objet correspondant à value, elle n'aura pas de référence forte et la prochaine fois que GC sera recyclé, ce qui entraînera une perte de données.

HéritableThreadLocal

InheritableThreadLocal hérite de ThreadLocal et est utilisé pour transférer des variables de thread entre les threads parent et enfant.

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

        itl.set("main");

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

Comme mentionné précédemment, un autre ThreadLocalMap dans le thread est utilisé pour InheritableThreadLocal.

Lors de la création d'un thread, si HeherableThreadLocals dans le thread parent n'est pas vide, il est transmis

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);
        
    }

Résumer

ThreadLocal est utilisé pour isoler les données entre les threads et peut stocker des données pour les utiliser dans leur contexte. Étant donné que les threads peuvent être réutilisés, ils doivent être supprimés après utilisation pour éviter toute confusion des données.

ThreadLocalMap est stocké dans le thread Thread. ThreadLocalMap est une table de hachage qui utilise la méthode d'adressage ouverte pour résoudre les conflits de hachage. La clé de stockage du nœud est ThreadLocal et la valeur stocke les données à stocker par le thread.

Le nœud hérite de la référence faible et définit ThreadLocal comme référence faible. Cela entraînera le recyclage du ThreadLocal la prochaine fois lorsque le GC ne sera plus utilisé. À ce moment, la clé sera vide. Si la valeur n'est plus utilisée, mais le nœud n'est pas supprimé, cela entraînera l'utilisation de la valeur, provoquant une fuite de mémoire

Dans les méthodes courantes de ThreadLocal telles que set, get, delete, etc., lors du parcours du tableau, revenez en arrière et supprimez les nœuds expirés (la clé est vide) pour éviter les fuites de mémoire.

Si ThreadLocal est défini sur une référence forte, des fuites de mémoire se produiront lorsque ThreadLocal n'est plus utilisé ; lorsque ThreadLocal est défini sur une référence faible, bien que des fuites de mémoire puissent également se produire, ces données peuvent être vérifiées et nettoyées à l'aide de méthodes courantes ; si la valeur est défini dans une référence faible, une perte de données se produira lorsque la couche externe n'utilise pas la valeur

InheritableThreadLocal hérite de ThreadLocal et est utilisé pour le transfert de données ThreadLocal entre les threads parent et enfant.

Enfin (ne le faites pas gratuitement, appuyez simplement trois fois de suite pour demander de l'aide~)

Cet article est inclus dans la colonne " Du point à la ligne et de la ligne à la surface " pour construire un système de connaissances en programmation simultanée Java en termes simples . Les étudiants intéressés peuvent continuer à y prêter attention.

Les notes et les cas de cet article ont été inclus dans gitee-StudyJava et github-StudyJava . Les étudiants intéressés peuvent continuer à y prêter attention sous stat~

Adresse du dossier :

Gitee-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

Github-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

Si vous avez des questions, vous pouvez en discuter dans la zone de commentaires. Si vous pensez que l'écriture de Cai Cai est bonne, vous pouvez l'aimer, la suivre et la collecter pour la soutenir ~

Suivez Cai Cai et partagez des informations plus utiles, compte public : la cuisine privée back-end de Cai Cai

Cet article est publié par OpenWrite, un blog qui publie plusieurs articles !

Lei Jun : La version officielle du nouveau système d'exploitation de Xiaomi, ThePaper OS, a été emballée. Une fenêtre contextuelle sur la page de loterie Gome App insulte son fondateur. Le gouvernement américain restreint l'exportation du GPU NVIDIA H800 vers la Chine. L'interface Xiaomi ThePaper OS est exposé. Un maître a utilisé Scratch pour frotter le simulateur RISC-V et il a fonctionné avec succès. Noyau Linux RustDesk Remote Desktop 1.2.3 publié, prise en charge améliorée de Wayland Après avoir débranché le récepteur USB Logitech, le noyau Linux a planté. DHH examen approfondi des "outils d'emballage" " : le front-end n'a pas du tout besoin d'être construit (No Build) JetBrains lance Writerside pour créer de la documentation technique Outils pour Node.js 21 officiellement publié
{{o.name}}
{{m.nom}}

Je suppose que tu aimes

Origine my.oschina.net/u/6903207/blog/10114997
conseillé
Classement