• 深入剖析ThreadLocal使用场景、实现原理、设计思想


    前言

    ThreadLocal可以用来存储线程的本地数据,做到线程数据的隔离

    ThreadLocal的使用不当可能会导致内存泄漏,排查内存泄漏的问题,不仅需要熟悉JVM、利用好各种分析工具还耗费人工

    如果能明白其原理并正确使用,就不会导致各种意外发生

    本文将从使用场景、实现原理、内存泄漏、设计思想等层面分析ThreadLocal,并顺带聊聊InheritableThreadLocal

    ThreadLocal使用场景

    什么是上下文?

    比如线程处理一个请求,请求会经过MVC流程,由于流程很长,会经历很多方法,这些方法就可以叫上下文

    ThreadLocal作用在上下文中存储常用的数据、存储会话信息、存储线程本地变量等

    比如使用拦截器在请求处理前,通过请求中的token获取登录用户信息,将用户信息存储在ThreadLocal中,方便后续处理请求时从ThreadLocal中直接获取用户信息

    如果线程会重复利用,为了避免数据错乱,使用完(拦截器处理后)应该删除该数据

    ThreadLocal 常用的方法有:set()get()remove()分别对应存储、获取和删除

    可以将ThreadLocal放在工具类中方便使用

    public class ContextUtils {
        public static final ThreadLocal USER_INFO_THREAD_LOCAL = new ThreadLocal();
    }
    • 1
    • 2

    拦截器伪代码

    //执行前 存储
    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();
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    使用时

    //提交订单
    public void orderSubmit(){
        //获取用户信息
        UserInfo userInfo = ContextUtils.USER_INFO_THREAD_LOCAL.get();
        //下单
        submit(userInfo);
        //删除购物车
        removeCard(userInfo);
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    为了更好的使用ThreadLocal,我们应该了解其实现原理,避免使用不当造成意外发生

    ThreadLocalMap

    Thread 线程中有两个字段存储ThreadLocal的内部类ThreadLocalMap

    public class Thread implements Runnable {    
    
        ThreadLocal.ThreadLocalMap threadLocals = null;
    
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    }
    • 1
    • 2
    • 3
    • 4
    • 5

    threadLocals用于实现ThreadLocal

    inheritableThreadLocals 用于实现InheritableThreadLocal (可继承的ThreadLocal 后文再聊)

    image.png

    ThreadLocalMap 的实现是哈希表,其内部类Entry是哈希表的节点,由Entry数组实现哈希表 ThreadLocalMap

    public class ThreadLocal {
        //,,,
        static class ThreadLocalMap {
            //...
            static class Entry extends WeakReference> {
                Object value;
    
                Entry(ThreadLocal k, Object v) {
                    super(k);
                    value = v;
                }
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    节点构造中的Key是ThreadLocal,Value是需要存储的值

    同时节点继承弱引用,通过泛型和构造可以知道它将ThreadLocal设置为弱引用

    不理解弱引用的同学可以查看这篇文章:深入浅出JVM(十四)之内存溢出、泄漏与引用 )

    image.png

    set

    在存储数据的方法中

    image.png

    获取ThreadLocalMap,如果没有则初始化ThreadLocalMap(懒加载)

    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);
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    createMap

    创建ThreadLocalMap赋值给当前线程的threadLocals

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

    创建ThreadLocalMap,初始化长度为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);
       }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    ThreadLocalMap.set

    通过哈希获取下标,当发生哈希冲突时,遍历哈希表(不再使用链地址法)直到位置上没有节点再进行构建

    遍历期间如果有节点,则根据节点取出key进行比较,如果是则是覆盖;如果节点没有key说明该节点的ThreadLocal被回收(已过期),为了防止内存泄漏会清理节点

    最后会检查其他位置有没有已过期的节点进行清理,并检查扩容

    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();
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    获取哈希值时,使用哈希值自增的原子类获取,步长则是每次自增的数量(也许是经过研究、测试的,尽量减少哈希冲突)

        //获取哈希值
        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);
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    nextIndex是获取下一个下标,超出上限时回到0

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

    get

    在获取数据时

    image.png

    获取当前线程的ThreadLocalMap,如果为空则初始化,否则获取节点

        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();
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在获取节点时,先根据哈希值获取到下标,再查看节点,比较key;如果匹配不上则说明key过期可能发生内存泄漏要去清理哈希表

            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);
            }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    内存泄漏

    在设置、获取数据的过程中,都会去判断key是否过期,如果过期就清理

    实际上ThreadLocal使用不当是会造成内存泄漏的

    设计者为了避免使用不当导致的内存泄漏,在常用方法中尽量清理这些过期的ThreadLocal

    前文说过节点继承弱引用,在构造中设置key为弱引用(也就是ThreadLocal)

    当ThreadLocal在任何地方都不被使用时,下次GC会将节点的key设置为空

    如果value也不再使用,但是由于节点Entry(null,value)存在,就无法回收value,导致出现内存泄漏

    image.png

    因此使用完数据后,尽量使用remove进行删除

    并且设计者在set、get、remove等常用方法中都会检查key为空的节点并删除,避免内存泄漏

    设计思想

    为什么要把entry中的key,也就是ThreadLocal设置成弱引用?

    我们先想象一个场景:线程在我们的服务中经常重复利用,而在某些场景下ThreadLocal并不长期使用

    如果节点entry 的key、value都是强引用,一但不再使用ThreadLocal,那么这个ThreadLocal还作为强引用存储在节点中,那么就无法回收,相当于发生内存泄漏

    把ThreadLocal设置为弱引用后,这种场景下如果value也不再使用依旧会发生内存泄漏,因此在set、get、remove方法中都会区检查删除key为空的节点,避免内存泄漏

    既然value可能无法回收,为什么不把value也设置成弱引用?

    由于value存储的是线程隔离的数据,如果将value设置成弱引用,当外层也不使用value对应的对象时,它就没有强引用了,再下次gc被回收,导致数据丢失

    InheritableThreadLocal

    InheritableThreadLocal 继承 ThreadLocal 用于父子线程间的线程变量传递

        public void testInheritableThreadLocal(){
            InheritableThreadLocal itl = new InheritableThreadLocal<>();
    
            itl.set("main");
    
            new Thread(()->{
                //main
                System.out.println(itl.get());
            }).start();
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    前文说过线程中另一个ThreadLocalMap就是用于InheritableThreadLocal 的

    在创建线程时,如果父线程中inheritableThreadLocals 不为空 则传递

    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);
    
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    总结

    ThreadLocal 用于隔离线程间的数据,可以存储数据作用在上下文中,由于线程可能重复利用,使用后需要删除,避免出现数据混乱

    Thread线程中存储ThreadLocalMap,ThreadLocalMap是一个使用开放定址法解决哈希冲突的哈希表,其中节点存储Key是ThreadLocal,Value存储的是线程要存储数据

    节点继承弱引用,并设置ThreadLocal为弱引用,这就导致当ThreadLocal不再使用时,下次GC会将其回收,此时Key为空,如果Value也不再使用,但是节点未删除就会导致value被使用,从而导致内存泄漏

    在ThreadLocal的set、get、remove等常用方法中,遍历数组的同时还回去将过期的节点(key为空)进行删除,避免内存泄漏

    如果将ThreadLocal设置成强引用,当ThreadLocal不再使用时会发生内存泄漏;将ThreadLocal设置成弱引用时,虽然也可能发生内存泄漏,但可以在常用方法中检查并清理这些数据;如果将value设置成弱引用,当外层不使用value时会发生数据丢失

    InheritableThreadLocal继承ThreadLocal ,用于父子线程间的ThreadLocal数据传递

    最后(不要白嫖,一键三连求求拉~)

    本篇文章被收入专栏 由点到线,由线到面,深入浅出构建Java并发编程知识体系,感兴趣的同学可以持续关注喔

    本篇文章笔记以及案例被收入 gitee-StudyJavagithub-StudyJava 感兴趣的同学可以stat下持续关注喔~

    案例地址:

    Gitee-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

    Github-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

    有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

    关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

    本文由博客一文多发平台 OpenWrite 发布!

  • 相关阅读:
    django-项目
    Base64、AES、MD5的区别与应用
    【数据结构与算法】之深入解析“长度为n的开心字符串中字典序第k小的字符串”的求解思路与算法示例
    适合上班族使用的电脑笔记软件使用哪一款
    【纯享】剑指大厂,22年最强阿里JAVA手册
    年费会员免费送
    css+js:实现tab切换线条跟随效果
    Windows平台下私有云盘搭建
    flex 收缩计算、简写
    修改history记录、定时校正服务器时间、停止IPv6网络服务、调整最大文件打开数、关闭写磁盘I/O功能、配置SSH服务
  • 原文地址:https://blog.csdn.net/Tc_lccc/article/details/133392374