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

这篇具有很好参考价值的文章主要介绍了深入剖析ThreadLocal使用场景、实现原理、设计思想。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

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

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

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

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

ThreadLocal使用场景

什么是上下文?

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

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

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

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

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

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

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

拦截器伪代码

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

使用时

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

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

ThreadLocalMap

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

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

threadLocals用于实现ThreadLocal

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

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

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

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

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

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

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

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

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

set

在存储数据的方法中

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

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

createMap

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

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

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

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

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

	//获取哈希值
    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是获取下一个下标,超出上限时回到0

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

get

在获取数据时

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

获取当前线程的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();
    }

在获取节点时,先根据哈希值获取到下标,再查看节点,比较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);
        }

内存泄漏

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

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

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

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

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

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

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

因此使用完数据后,尽量使用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<String> itl = new InheritableThreadLocal<>();

        itl.set("main");

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

前文说过线程中另一个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);
        
    }

总结

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-StudyJava、 github-StudyJava 感兴趣的同学可以stat下持续关注喔~

案例地址:

Gitee-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

Github-JavaConcurrentProgramming/src/main/java/G_ThreadLocal

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

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

本文由博客一文多发平台 OpenWrite 发布!文章来源地址https://www.toymoban.com/news/detail-711994.html

到了这里,关于深入剖析ThreadLocal使用场景、实现原理、设计思想的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深入理解 python 虚拟机:字节码教程(3)——深入剖析循环实现原理

    在本篇文章当中主要给大家介绍 cpython 当中跟循环相关的字节码,这部分字节码相比起其他字节码来说相对复杂一点,通过分析这部分字节码我们对程序的执行过程将会有更加深刻的理解。 我们使用各种例子来理解和循环相关的字节码: 上面的代码对应的字节码如下所示:

    2023年04月15日
    浏览(28)
  • 深入篇【C++】手搓模拟实现list类(详细剖析底层实现原理)&&模拟实现正反向迭代器【容器适配器模式】

    1.一个模板参数 在模拟实现list之前,我们要理解list中的迭代器是如何实现的。 在vector中迭代器可以看成一个指针,指向vector中的数据。它的解引用会访问到具体的数据本身,++会移动到下一个数据位置上去,这些都是因为vector具有天生的优势:空间上是连续的数组,这样指

    2024年02月15日
    浏览(33)
  • 闪电网络协议设计思想剖析

    闪电网络可能是比特币之上部署的最受期待的技术创新。闪电网络,为由 Joseph Poon 和 Tadge Dryja 于2015年首次提出的支付层,承诺支持: 用户之间几乎无限数量的链下交易, 几乎免费, 同时利用比特币提供的安全性。 2016年时,至少三个公司——Poon 和 Dryja 的 Lightning、 Block

    2024年03月20日
    浏览(50)
  • Vue3设计思想及响应式源码剖析

    对TypeScript支持不友好(所有属性都放在了this对象上,难以推倒组件的数据类型) 大量的API挂载在Vue对象的原型上,难以实现TreeShaking。 架构层面对跨平台dom渲染开发支持不友好,vue3允许自定义渲染器,扩展能力强。 CompositionAPI。受ReactHook启发 对虚拟DOM进行了重写、对模板的

    2024年02月05日
    浏览(33)
  • 深入云原生—基于KubeWharf深度剖析-以公司实际应用场景为例深度解读

    各位好,这里是难忘,本人对云原生也是研究了2年多了,算是略有所得,本次就来深入云原生—基于KubeWharf深度剖析场景与解读。我们需要先了解一下 KubeWharf,可能很多人都感觉到有点陌生吧,下面我们来一起学习! KubeWharf 是字节跳动基础架构团队在对 Kubernetes 进行了大

    2024年01月23日
    浏览(46)
  • SpringBoot 原理深入及源码剖析

    SpringBoot 依赖管理 SpringBoot 项目间接继承 spring-boot-dependencies,该文件对常用技术框架进行了统一版本管理,所以在SpringBoot 项目 pom.xml 引入spring-boot-dependencies管理的依赖文件不需要标注依赖文件版本号。引入 starter 就可以实现对应场景开发,而不需要额外导入相关依赖文件。

    2024年02月02日
    浏览(66)
  • 深入剖析 Git 对象底层原理

    在我们日常使用 Git 时,通常的操作是: 在写完一段代码后,执行 git add 命令,将这段代码添加到暂存区中 然后再执行 git commit 和 git push 命令,将 本地 Git 版本库中的提交同步到服务器中的版本库中 Git 在中间做了什么,它如何存储不同的文件和内容,以及如何区分不同分支

    2024年01月20日
    浏览(34)
  • 深入剖析 Qt QMap:原理、应用与技巧

    QMap是Qt框架中的一个关联容器类,用于存储键值对。它提供了高效且易于使用的方法来处理键值对数据,使得开发者可以在各种实际场景中轻松地存储和检索数据。QMap内部使用平衡二叉树(红黑树)作为底层数据结构,提供了高效的插入、删除和查找操作。掌握QMap及其基本

    2023年04月21日
    浏览(37)
  • 深入剖析 Qt QMultiMap :原理、应用与技巧

    随着软件开发的不断演进,数据结构在程序设计中扮演着至关重要的角色。它们为开发人员提供了丰富的工具来有效地管理和操纵数据。在这个博客中,我们将重点关注一种特殊的数据结构——QMultiMap,这是Qt框架中提供的一个强大且实用的容器类。在接下来的文章中,我们

    2023年04月21日
    浏览(25)
  • 深入剖析mmap原理 - 从三个关键问题说起

    作者:招财二师兄 链接:https://www.jianshu.com/p/eece39beee20 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 对于mmap,您是否能从原理上解析以下三个问题: 1:mmap比物理内存+swap空间大情况下,是否有问题? 2:MAP_SHARED,MAP_PRIVATE,MAP_

    2024年02月09日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包