当前位置: 首页 > news >正文

ThreadLocal源码解析

使用ThreadLocal可以为每个线程维护一个线程变量,使用场景为线程间隔离,线程内方法共享;

原理:

Thread类中有一个实例属性ThreadLocalMap,ThreadLocalMap中存放的是Entry数组,Entry数组是ThreadLocal和Object的键值对;源码如下--

Thread类:

/** ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals;

ThreadLocal->ThreadLocalMap->Entry类:

static class ThreadLocalMap {/*** The entries in this hash map extend WeakReference, using* its main ref field as the key (which is always a* ThreadLocal object).  Note that null keys (i.e. entry.get()* == null) mean that the key is no longer referenced, so the* entry can be expunged from table.  Such entries are referred to* as "stale entries" in the code that follows.*/static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}

ThreadLocal有一个内部静态类ThreadLocalMap, ThreadLocalMap是一个Entry数组,通过map方法来访问数据,而Entry对key(ThreadLocal实例对象)的引用使用了虚引用;这里导致key可能会在JVM判断oom之前将key回收掉,导致内存泄漏-此时key对应的ThreadLocal对象被回收变成null,而value存在Thread reference->thread->threadlocalMap->entry-value的强引用链,导致无法回收;虽然在ThreadLocal的get和set以及remove方法中实现了检查是否存在key为null并清理value的机制,但是无法避免内存短时间内泄露的问题;

看一下ThreadLocal的set()和get()方法--

public void set(T value) {set(Thread.currentThread(), value);}private void set(Thread t, T value) {ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}
public T get() {return get(Thread.currentThread());}private T get(Thread t) {//获取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {T result = (T) e.value;return result;}}return setInitialValue(t);}private Entry getEntry(ThreadLocal<?> key) {//这里进行map操作和hashmap的原理基本是一样的,hashcode按位与数组长度减一,数组长度默认为16,(2的n次方减一值做按位与相当于取模,保证了散列均衡)int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.refersTo(key))return e;elsereturn getEntryAfterMiss(key, i, e);}

所以,ThreadLocal维护为每一个线程维护线程变量副本的原理在于,每个线程对象会创建自己的THreadLocalMap--这里并非主动创建,而是调用THreadLocal的set或者get方法的时候才创建;而ThreadLocalMap则是一个以ThreadLocal实例对象的弱应用为key,Object对象为value的键值对数组,以便每个线程对象存放多个线程变量,而对这个线程变量的访问是通过ThreadLocal对象来操作的,所以说实现线程变量的根本不在于ThreadLocal,而是ThreadLocalMap,而ThreadLocal对象只是访问该线程的ThreadLocalMap的一个入口,由于get(Thread t)和set(Thread t,T value)都是私有方法,通过其他对象无法访问,所以只能通过ThreadLocal对象提供的重载方法get()和set(Object value)来访问,保证了线程变量的安全性---其他线程无法修改另外一个线程的ThreadLocalMap;所以核心是getMap(Thread t),而这里的线程对象t只能是Thread.currentThread;

另外需要注意的是,对ThreadLocal 保存的线程变量一定要在使用完之后及时remove掉,一个原因是防止内存泄漏,更重要的原因是在线程池场景下,一个请求可能会读取到上一个请求保存的变量内容,从而造成业务逻辑上的BUG,而且这种BUG还是不易排查的;

另外关于内存泄露的问题JDK提供了一些保障,分为启发式清理和探测式清理--

探测式清理--

原理:

  1. 删除当前元素;
  2. 继续往后遍历,
    1. 当key=null的时候,删除value
    2. 当key!=null的时候,将因哈希碰撞后移的元素重新放置----ThreadLocalMap中使用了后移解决哈希碰撞

源码:

private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 删除当前元素--value置为null,value引用的对象变成了无引用对象,会被回收;,同样的将数组上该位置的Entry对象置空,原来的Entry对象也会被回收掉;tab[staleSlot].value = null;tab[staleSlot] = null;size--;// Rehash until we encounter nullEntry e;int i;//继续遍历for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//key为空的时候继续删除if (k == null) {e.value = null;tab[i] = null;size--;} else {//key 不为空的时候rehash,重新计算该key的下标---这里需要注意--rehash的结果可能和原来的值是一样的,因为不能完全保证是否产生过哈希碰撞int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}

启发式清理--

        往后遍历,如果找到key为null的过期元素, 则调用探测式清理--启发式清理可以理解为为探测式清理找到一个清理入口,由探测式清理完成具体的清理工作

private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;do {i = nextIndex(i, len);Entry e = tab[i];if (e != null && e.refersTo(null)) {n = len;removed = true;i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);return removed;}

另外看一下清理工作在哪里被调用--

  1. set(Thread t,T value)->cleabSomeSlots();
  2. set(Thread t,T value)->replaceStaleEntry()->cleanSomeSlots();
  3. get(t)->getEntry()->getEntryAfterMiss()->expungeStaleEntry();
  4. remove()->expungeStaleEntry();
  5. rehash()->expungeStaleEntries->expungeStaleEntry()

上边的调用关系可以看出来基本上我们对ThreadLocal的get,set,remove操作都会触发清理工作,尽量减小了内存泄漏带来的影响---在未做任何操作之前不会主动触发清理,所以在使用ThreadLocal的时候及时调用remove很重要!!!

http://www.lryc.cn/news/123873.html

相关文章:

  • RocketMQ 5.1.0 源码详解 | Producer 发送流程
  • 电脑ip地址怎么改 ip地址怎么改到别的城市
  • Android Studio实现列表展示图片
  • 每天一道leetcode:300. 最长递增子序列(动态规划中等)
  • 【无监督】2、MAE | 自监督模型提取的图像特征也很能打!(CVPR2022 Oral)
  • pytorch单机多卡后台运行
  • linux配置上网 linux adsl拨号上网设置
  • XML学习基础知识归纳(一)
  • 2023.8.14论文阅读
  • FL Studio for Windows-21.1.0.3713中文直装版功能介绍及系统配置要求
  • 基于网格变形的二维图像变形算法:C++实现与应用
  • 【数据结构】八大排序详解
  • VSCode如何设置高亮
  • 密钥大全ubuntu
  • Spring Task入门案例
  • 针对Android项目蓝牙如何学习
  • C++学习笔记总结练习:内存分配器编程实现
  • 【uniapp】使用Vs Code开发uniapp:
  • 【STM32】高效开发工具CubeMonitor快速上手
  • React 使用 i18n 翻译换行解决方法
  • QEMU源码全解析37 —— Machine(7)
  • 如何将阿里云WiredTiger引擎的MongoDB物理备份文件恢复至自建数据库
  • SAP FIORI Launchpad 403 forbidden error
  • 【MongoDB】高性能非关系型数据库
  • 二、MySql库的操作
  • 【ARM 嵌入式 编译系列 10 -- GCC 编译缩减可执行文件 elf 文件大小】
  • IDEA启动报错java.nio.charset.MalformedInputException: Input length=2
  • 【Vue-Router】路由传参
  • 平板选择什么电容笔比较好?ipad手写笔推荐品牌
  • 什么是数字化车间