ThreadLocal源码分析及内存泄漏

news2024/10/4 17:21:24

ThreadLocal原理分析及内存泄漏

    • ThreadLocal的使用
    • ThreadLocal原理
      • set方法解析
      • replaceStaleEntry方法解析
      • expungeStaleEntry方法解析
      • cleanSomeSlots方法解析
        • case 1: 向前有脏数据,向后找到可覆盖的Entry
        • case 2: 向前有脏数据,向后未找到可覆盖的Entry
        • case 3: 向前没有脏数据,向后找到可覆盖的Entry
        • case 4: 向前没有脏数据,向后未找到可覆盖的Entry
      • get方法解析
    • ThreadLocal内存泄漏
    • Why key is ThreadLocal?

ThreadLocal意为线程本地变量,它会在每个线程创建一个数据副本,所以可以用来解决多线程并发时访问共享变量的问题。

ThreadLocal的使用

  • set()
    在当前线程范围内,设置一个值存储到ThreadLocal中,这个值仅对当前线程可见。
    相当于在当前线程范围内建立了副本。
  • get()
    从当前线程范围内取出set方法设置的值.
  • remove()
    移除当前线程中存储的值

ThreadLocalMap里的Entry使用的key是对ThreadLocal对象的弱引用, 当没有强引用来引用ThreadLocal实例的时候,JVM的GC会回收ThreadLocalMap中的这些key
在这里插入图片描述

ThreadLocal原理

ThreadLocal 能够实现线程间的隔离,所以当前线程保存的数据,只会存储在当前线程范围内。-> 数据是线程私有的 --> 每个线程有自己的ThreadLocalMap -> key 为ThreadLocal对象

set方法解析

   public void set(T value) {
        Thread t = Thread.currentThread();
        // 如果当前线程已经初始化了map,则获取这个map,没有则进行初始化
        ThreadLocalMap map = getMap(t);
        if (map != null) //修改value
            map.set(this, value);
        else //初始化并设置value值
            createMap(t, value);
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
		table = new Entry[INITIAL_CAPACITY]; //默认长度为16的数组
		int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //计算数组下标
		table[i] = new Entry(firstKey, firstValue); //把key/value存储到下标为i的位置.
		size = 1;
		setThreshold(INITIAL_CAPACITY);
	}
    //注意下这里的Entry对ThreadLocal是一个弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 private void set(ThreadLocal<?> key, Object value) {
        // We don't use a fast path as with get() because it is at
        // least as common to use set() to create new entries as
        // it is to replace existing ones, in which case, a fast
        // path would fail more often than not.
        Entry[] tab = table;
        int len = tab.length;
        //计算数组下标
        int i = key.threadLocalHashCode & (len-1);
        //线性探索,从i开始向后探索,如果遇到相同的key可以替换value后退出;如果遇到key==null就需要清理旧entry,在探索过程中,如果有个entry==null那么就说明这里有个空位,直接存放数据就行了(结束循环)
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            //如果在i的位置存在key且和当前要设置的key是一个,则直接替换。
            if (k == key) {
                e.value = value;
                return;
            }
            //如果key==null,则认为这个位置可能之前存放过旧的entry,不能直接赋值replaceStaleEntry(清理过期数据并将当前value保存到entry数组中) 放到后面单独分析下
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        //执行到这里就说明tab[i]上没有数据,可以存放entry(注意前面的循环结束条件)  一定会有空位的,否则在上次新增entry的时候会触发扩容
        tab[i] = new Entry(key, value);
        int sz = ++size;
        //cleanSomeSlots返回真说明有stale entry被清空了,size肯定减小了;
        //只有当 cleanSomeSlots返回假 且到达阈值时,才肯定需要rehash
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

ThreadLocal中的线性探索
1.向前/向后查找stale的节点
2.向后查找可覆盖的节点 (可以结合后面的图来理解)
3.清理节点并调整部分节点的位置

replaceStaleEntry方法解析

    //staleSlot是当前key==null的下标(就是一个可能已过期的下标位置,对应的key可能被GC回收了) 可以结合下面的图来理解这个
    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                   int staleSlot) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        ThreadLocal.ThreadLocalMap.Entry e;

        //记录下可能需要被清除的index(从stableSlot开始),向前探索直到找到entry==null的位置
        int slotToExpunge = staleSlot;
        for (int i = prevIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;

        //向后探索,去查找key==目标key的位置,遇到tab[i]==null则结束循环
        for (int i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();

            //找到后将staleSlot和当前位置i的元素互换
            if (k == key) {
                e.value = value;

                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;

                // 如果相同,则说明staleSlot之前没有待清理的entry,这个条件只会成立一次
                if (slotToExpunge == staleSlot)
                    //(1)这里的slotToExpunge其实等于nextIndex(staleSlot, len)
                    slotToExpunge = i;
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }

            // If we didn't find stale entry on backward scan, the
            // first stale entry seen while scanning for key is the
            // first still present in the run.
            //如果staleSlot之前没有待清理的entry(slotToExpunge == staleSlot)且k==null,说明位置i的entry需要清理,此时的slotToExpunge为staleSlot最近的一个需要清理的entry下标
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        // If key not found, put new entry in stale slot
        //如果没有找到可覆盖的key,直接将staleSlot的位置清理到,存放现在的值就行
        tab[staleSlot].value = null;
        tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

        // 如果探索过程中有发现其他stale节点,则清理它们
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

expungeStaleEntry方法解析

   private int expungeStaleEntry(int staleSlot) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        // staleSlot节点的数据是一定要清理的,因为其key==null
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;
        // Rehash until we encounter null
        ThreadLocal.ThreadLocalMap.Entry e;
        int i;
        //从staleSlot向后遍历开始清理节点(遇到entry==null的情形,退出清理)
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            //清理k==null的节点
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                //k!=null,由于清理了前面的数据,那么就重新计算下标,将节点tab[i]存放到更接近正确下标的位置,方便下次查询
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    //移动tab[i]的entry到更前面的位置,所以这里可以置为null
                    tab[i] = null;
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        //i为结束循环的下标,tab[i]==null(i是staleSlot之后第一个为entry==null的位置)
        return i;
    }

cleanSomeSlots方法解析

    //从entry==null的位置开始清理(前面的数组在expungeStaleEntry()里清理了)
    private boolean cleanSomeSlots(int i, int n) {
        //设置清空标志,是否清除过数据
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        //循环清除,最少循环log2(n)次
        do {
            i = nextIndex(i, len);
            Entry e = tab[i];
            //找到key为null的节点,则进行清除
            if (e != null && e.get() == null) {
                //重新赋值最新的len给n,如果有找到可以清除的数据,那么n就会恢复到len,又重新开始清理
                n = len;
                removed = true;
                //从i开始查找下一个tab[i]==null的下标
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0); //无符号右移,若没有找到任何可以清理的数据,则循环执行log2(n)次
        //返回清除标志
        return removed;
    }

总结下这里的清理逻辑:

向前找key == null的第一个脏数据(没有则向后找第一个脏数据,记为slotToExpunge),向后找可覆盖的entry(有则替换), 如果找到脏数据,那么就从slotToExpunge 开始清理(最终目标是将从slotToExpunge 到len的数据都清理一遍)

清理的时候会将key==null的entry的value也置为空,同时会将清理过程中遇到的key!=null的节点调整位置

case 1: 向前有脏数据,向后找到可覆盖的Entry

在这里插入图片描述
在这里插入图片描述

case 2: 向前有脏数据,向后未找到可覆盖的Entry

在这里插入图片描述

case 3: 向前没有脏数据,向后找到可覆盖的Entry

在这里插入图片描述

case 4: 向前没有脏数据,向后未找到可覆盖的Entry

在这里插入图片描述
为什么在进行线性探索的时候遇到key==null的entry不可以直接存储到该位置?

前面有提到ThreadLocalMap里的Entry使用的key是对ThreadLocal对象的弱引用, 当没有强引用来引用ThreadLocal实例的时候,JVM的GC会回收ThreadLocalMap中的这些key,此时,ThreadLocalMap中就会出现一些key为null,但是value不为null(也可能用户设置的value就是null)的Entry,这些Entry如果用户不主动清理,就会一直保留在ThreadLocalMap中。

ThreadLocal底层数据是用一个entry数组来存储的,key为ThreadLocal对象,value为用户设置的值
在使用hashCode来计算索引的时候肯定会有hash冲突的问题,如果有hash冲突(假设为位置i),那么新来的key只能存放在i的下一个为空的位置。当你发现table[i]上的keynull,它可能是没有存储过任何数据,也可能是原来的key被GC回收了,所以就需要进行线性探索,判断后续的数组位置上是否有key目前对象的数据,有的话需要替换下标。

有人会问,为什么找到相同key的位置后需要替换呢,因为在set()的时候会判断tab[i]==null,如果我们把数据存放在正确位置i的后面,等下一次再set的时候,如果tab[i]==null,那么就会直接保存在i的位置上,导致一个Map里存在相同key

get方法解析

    public T get() {
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //查找对应的value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果没有存储过,则返回一个初始化值
        return setInitialValue();
    }
     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
                //如果没有在正确的index下找到该数据,可能是发生hash冲突导致位置发生了改变
                return getEntryAfterMiss(key, i, e);
        }
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
        //从i开始向后遍历,因为set的时候也是set到第一个不为null的值,所以只需要遍历到第一个为null的位置即可
        while (e != null) {
            ThreadLocal<?> k = e.get();
            //key相等,直接返回value
            if (k == key)
                return e;
            //key为null,说明该Entry是 stale节点,需要清除
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

ThreadLocal内存泄漏

当ThreadLocal被回收后,ThreadLocalMap中对应的key就会指向null,而对应value却不为null,这些value项如果不主动清理,就会一直驻留在ThreadLocalMap中。

从前面的源码分析,可以知道 expungeStaleEntry() 方法是帮助垃圾回收的,根据源码,我们可以发现
get 和set 方法都可能触发清理方法 expungeStaleEntry() ,所以正常情况下是不会有内存溢出的 但
是如果我们没有调用get 和set 的时候就会可能面临着内存溢出,所以在使用ThreadLocal的时候可以在不再需要该对象的时候手动调用remove()方法,加快垃圾回收,避免内存溢出

一般情形下,线程结束的时候,也就没有强引用再指向ThreadLocal 中的ThreadLocalMap了,这样ThreadLocalMap 和里面的元素也会和线程一起被回收掉,但是当你使用的线程是线程池时, 由于这个线程在执行完的时候并不会销毁,而是归还给线程池,就会出现内泄漏的问题。

Why key is ThreadLocal?

在JDK早期的设计中,每个ThreadLocal都有一个map对象,将线程作为map对象的key,要存储的变量作为map的value,但是现在已经不是这样了。

JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的要隔离的变量(这里的key不能是Thread,因为一个线程可能会定义多个ThreadLocal), Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;

JDK8之后设计的好处在于:

  1. 每个Map存储的Entry的数量变少,在实际开发过程中,ThreadLocal的数量往往要少于Thread的数量,Entry的数量减少就可以减少哈希冲突。
  2. 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用,早期的ThreadLocal并不会自动销毁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/421764.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

吴恩达机器学习--线性回归

文章目录前言一、单变量线性回归1.导入必要的库2.读取数据3.绘制散点图4.划分数据5.定义模型函数6.定义损失函数7.求权重向量w7.1 梯度下降函数7.2 最小二乘法8.训练模型9.绘制预测曲线10.试试正则化11.绘制预测曲线12.试试sklearn库二、多变量线性回归1.导入库2.读取数据3.划分…

掌握高效绘制地图的利器——LeafletJs

文章目录前言一、leafletJs是什么&#xff1f;二、快速入门1、安装2、快速入门三、进阶学习1、Map 控件2、Marker 标记3、Popup 弹出窗口4、图层四、项目实战封装文件4.1 基础点位图4.2 行驶轨迹图前言 GIS 作为获取、存储、分析和管理地理空间数据的重要工具&#xff0c;用 G…

数据结构与算法一览(树、图、排序算法、搜索算法等)- Review

算法基础简介 - OI Wiki (oi-wiki.org) 文章目录1. 数据结构介绍1.1 什么是数据结构1.2 数据结构分类2. 链表、栈、队列&#xff1a;略3. 哈希表&#xff1a;略4. 树4.1 二叉树4.2 B 树与 B 树4.3 哈夫曼&#xff08;霍夫曼&#xff09;树&#xff1a;Huffman Tree4.4 线段树&a…

编辑文件/文件夹权限 - Win系统

前言 我们经常会遇到由于权限不够无法删除文件/文件夹的情况&#xff0c;解决方案一般是编辑文件/文件夹的权限&#xff0c;使当前账户拥有文件的完全控制权限&#xff0c;然后再进行删除&#xff0c;下文介绍操作步骤。 修改权限 查看用户权限 右键文件/文件夹&#xff0c;…

(函数指针) 指向函数的指针

函数指针- 指向函数的指针函数指针的声明和使用通过函数指针调用函数函数指针做参数函数指针数组函数指针的声明和使用 函数指针的声明格式&#xff1a; 返回值类型 (*函数指针名)(参数列表); 其中&#xff1a; *函数指针名 表示函数指针的名称返回值类型 则表示该指针所指向…

【Kubernetes】StatefulSet对象详解

文章目录简介1. StatefulSet对象的概述、作用及优点1.1 对比Deployment对象和StatefulSet对象1.2 以下是比较Deployment对象和StatefulSet对象的优缺点&#xff1a;2. StatefulSet对象的基础知识2.1 StatefulSet对象的定义2.1.1 下表为StatefulSet对象的定义及其属性&#xff1…

上岸川大网安院

一些感慨 一年多没写过啥玩意了&#xff0c;因为考研去了嘿嘿。拟录取名单已出&#xff0c;经历一年多的考研之路也可以顺利打上句号了。 我的初试成绩是380&#xff0c;政治65&#xff0c;英语81&#xff0c;数学119&#xff0c;专业课115。 回顾这一路&#xff0c;考研似乎也…

分类预测 | MATLAB实现CNN-BiLSTM-Attention多输入分类预测

分类预测 | MATLAB实现CNN-BiLSTM-Attention多输入分类预测 目录分类预测 | MATLAB实现CNN-BiLSTM-Attention多输入分类预测分类效果基本介绍模型描述程序设计参考资料分类效果 基本介绍 MATLAB实现CNN-BiLSTM-Attention多输入分类预测&#xff0c;CNN-BiLSTM结合注意力机制多输…

Vue3使用Vant组件库避坑总结

文章目录前言一、问题二、解决方法三、问题出现原因总结经验教训前言 本片文章主要写了&#xff0c;Vue3开发时运用Vant UI库的一些避坑点。让有问题的小伙伴可以快速了解是为什么。也是给自己做一个记录。 一、问题 vue3版本使用vant失败&#xff0c;具体是在使用组件时失效…

IPBX系统快速部署和Freeswitch 1.10.7自动安装

IPBX系统部署文档 IPPBX系统 1.10.7版本Freeswitch &#xff0c;手机互联互通&#xff0c;SIP协议&#xff0c;分机互相拨打免费通话清晰&#xff0c;支持wifi或4G网络互相拨打电话&#xff0c;可以对接OLT设备&#xff0c;系统可以部署到本地物理机&#xff0c;也可以部署到阿…

工程质量之研发过程管理需要关注的点

一、背景 作为程序猿&#xff0c;工程质量是我们逃不开的一个话题&#xff0c;工程质量高带来的好处多多&#xff0c;我在写这篇文章的时候问了一下CHATGPT&#xff0c;就当娱乐一下&#xff0c;以下是ChatGPT的回答&#xff1a; 1、提高产品或服务的可靠性和稳定性。高质量的系…

光时域反射仪那个品牌的好用

光时域反射仪 哪个品牌好用 光时域反射仪要怎么选到合适自己的&#xff0c;这些问题 可能一直在困扰这一线的工作人员&#xff0c;下面小编就为大家一一解答下 首先光时域域反射仪是一款检测光纤线路的损耗 长度 以及 事件点的一款设备&#xff0c;在诊断 光纤线路 故障点的情…

从零开始学架构——CAP理论

CAP定理 CAP 定理&#xff08;CAP theorem&#xff09;又被称作布鲁尔定理&#xff08;Brewer’s theorem&#xff09;&#xff0c;是加州大学伯克利分校的计算机科学家埃里克布鲁尔&#xff08;Eric Brewer&#xff09;在 2000 年的 ACM PODC 上提出的一个猜想。2002 年&…

Web前端 HTML、CSS

HTML与CSSHTML、CSS思维导图一、HTML1.1、HTML基础文本标签1.2、图片、音频、视频标签1.3、超链接、表格标签1.4、布局1.5、表单标签1.6、表单项标签综合使用1.7、HTML小结二、CSS&#xff08;简介&#xff09;2.1、引入方式2.2、选择器2.3、CSS属性Web前端开发总览 Html&…

案例拆解丨ChatGPT+塔罗牌,批量起号、暴利引流,小白也能轻松月入10000+

ChatGPT 的出现&#xff0c;大大拉低了很多行业的门槛&#xff0c;比如客服、教育、翻译、自媒体……而塔罗牌占卜&#xff0c;肯定也是其中之一。 塔罗牌是一种占卜工具&#xff0c;由78张牌组成。可以用于占卜、灵性探索、个人成长和自我发现。 这是一个相对小众&#xff0c…

LinuxGUI自动化测试框架搭建(十三)-创建工具集目录tools并封装文件复制方法cpoyFile.py

(十三)-创建工具集目录tools并封装文件复制方法cpoyFile.py 1 tools的作用2 创建tools目录3 创建文件复制方法cpoyFile.py4 设计cpoyFile.py4.1 安装shutil4.2 导入模块4.3 脚本设计5 目前框架目录1 tools的作用 为了存放框架需要用到的一些常用工具或方法,比如文件复制功能…

OJ系统刷题 第九篇(难篇)

13441 - 求小数的某一位&#xff08;难题&#xff0c;二刷、三刷&#xff01;&#xff09; 时间限制 : 1 秒 内存限制 : 128 MB 分数\tfrac {a}{b}ba​化为小数后&#xff0c;小数点后第n位的数字是多少&#xff1f; 输入 三个正整数a&#xff0c;b&#xff0c;n&#xff0…

使用jni-rs实现Rust与Android代码互相调用

本篇主要是介绍如何使用jni-rs。有关jni-rs内容基于版本0.20.0&#xff0c;新版本写法有所不同。 入门用法 在Rust库交叉编译以及在Android与iOS中使用中我简单说明了jni-rs及demo代码&#xff0c;现在接着补充一些详细内容。 首先贴上之前的示例代码&#xff1a; use std:…

嘉靖王朝最大的一出闹剧和惨剧——大礼仪之争

大礼仪之争 大礼议是指发生在正德十六年&#xff08;1521年&#xff09;到嘉靖三年&#xff08;1524年&#xff09;间的一场皇统问题上的政治争论。 原因是明世宗以地方藩王入主皇位&#xff0c;为其改换父母的问题所引起&#xff0c;是明朝历史第二次小宗入大宗的事件。 “…

罗丹明荧光染料标记叶酸,FA-PEG-RB,叶酸-聚乙二醇-罗丹明;Folic acid-PEG-RB

FA-PEG-RB,叶酸-聚乙二醇-罗丹明 中文名称&#xff1a;叶酸-聚乙二醇-罗丹明 英文名称&#xff1a;FA-PEG-RB, Folic acid-PEG-RB 性状&#xff1a;粉红色固体或液体&#xff0c;取决于分子量 溶剂&#xff1a;溶于水和DMSO、DMF等常规性有机溶剂 保存条件&#xff1a;-2…