ThreadLocal 实现原理

简介: ThreadLocal 实现原理
ThreadLocal 实现原理


ThreadLocalget(),set()的时候都会清除线程ThreadLocalMap里所有keynullvalue。而ThreadLocal的remove()方法会先将Entry中对key的弱引用断开,设置为null,然后再清除对应的key为null的value。

每一个Thread维护一个ThreadLocalMap映射表,映射表的keyThreadLocal实例,并且使用的是ThreadLocal的弱引用 ,value是具体需要存储的Object。下面用一张图展示这些对象之间的引用关系,实心箭头表示强引用,空心箭头表示弱引用。

image.png

ThreadLocal local = new ThreadLocal();
local.set("当前线程名称:"+Thread.currentThread().getName());//将ThreadLocal作为key放入threadLocals.Entry中
Thread t = Thread.currentThread();//注意断点看此时的threadLocals.Entry数组刚设置的referent是指向Local的,referent就是Entry中的key只是被WeakReference包装了一下
local = null;//断开强引用,即断开local与referent的关联,但Entry中此时的referent还是指向Local的,为什么会这样,当引用传递设置为null时无法影响传递内的结果
System.gc();//执行GC
t = Thread.currentThread();//这时Entry中referent是null了,被GC掉了,因为Entry和key的关系是WeakReference,并且在没有其他强引用的情况下就被回收掉了
//如果这里不采用WeakReference,即使local=null,那么也不会回收Entry的key,因为Entry和key是强关联
//但是这里仅能做到回收key不能回收value,如果这个线程运行时间非常长,即使referent GC了,value持续不清空,就有内存溢出的风险
//彻底回收最好调用remove
//即:local.remove();//remove相当于把ThreadLocalMap里的这个元素干掉了,并没有把自己干掉
System.out.println(local);


使用InheritableThreadLocal,ThreadLocal threadLocal = new InheritableThreadLocal(),这样在子线程中就可以通过get方法获取到主线程set方法设置的值了。

ThreadLocalMap 结构


static class ThreadLocalMap {
 /**
  * 键值对实体的存储结构
  */
 static class Entry extends WeakReference<ThreadLocal<?>> {
  // 当前线程关联的 value,这个 value 并没有用弱引用追踪
  Object value;
  /**
   * 构造键值对
   *
   * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
   * @param v v 作 value
   */
  Entry(ThreadLocal<?> k, Object v) {
   super(k);
   value = v;
  }
 }
 // 初始容量,必须为 2 的幂
 private static final int INITIAL_CAPACITY = 16;
 // 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂
 private Entry[] table;
 // ThreadLocalMap 元素数量
 private int size = 0;
 // 扩容的阈值,默认是数组大小的三分之二
 private int threshold;
}

Java 不同的是,采用的是线性探测法。


private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;     // 以上代码,将entry的value赋值为null,这样方便GC时将真正value占用的内存给释放出来;将entry赋值为null,size减1,这样这个slot就又可以重新存放新的entry了
    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); // 从staleSlot后一个index开始向后遍历,直到遇到为null的entry
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {    // 如果entry的key为null,则清除掉该entry
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {   // key的hash值不等于目前的index,说明该entry是因为有哈希冲突导致向后移动到当前index位置的
                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)      // 对该entry,重新进行hash并解决冲突
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;   // 返回经过整理后的,位于staleSlot位置后的第一个为null的entry的index值
}

在从第i个entry向后遍历的过程中,找到对应的key的entry就直接返回,如果遇到key为null的entry,则调用expungeStaleEntry方法进行清理。

ThreadLocalMap set方法

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);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                    e.value = value;
                    return;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

ThreadLocalMap remove 方法


找到准确的key对应的entry之后,调用Entry的clear方法,紧接着调用expungeStaleEntry,对key为null的entry进行清理。

private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {       // 考虑到可能的哈希冲突,一定要准确找到此key对应的entry
            e.clear();  // 调用Entry的clear方法,见代码2
            expungeStaleEntry(i);       // 又是这个清除key为null的entry的方法,见代码3
            return;
        }
    }
}

平时怎么使用

看个测试代码

package thread;
public class ThreadLocalDemo {
    /**
     * ThreadLocal变量,每个线程都有一个副本,互不干扰
     */
    public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
    public static void main(String[] args) throws Exception {
        new ThreadLocalDemo().threadLocalTest();
    }
    public void threadLocalTest() throws Exception {
        // 主线程设置值
        THREAD_LOCAL.set("mainthreadvalue");// 设置的key 是主线程
        String v = THREAD_LOCAL.get();
        System.out.println("Thread-0线程执行之前," + Thread.currentThread().getName() + "线程取到的值:" + v);
        new Thread(new Runnable() {
            @Override
            public void run() {
                String v = THREAD_LOCAL.get(); // 获取的是key 是当前线程 渠道的是 null
                System.out.println(Thread.currentThread().getName() + "线程取到的值:" + v);
                // 设置 threadLocal
                THREAD_LOCAL.set("inThreadValue"); // 设置的 key 是当前咸亨
                v = THREAD_LOCAL.get();
                System.out.println("重新设置之后," + Thread.currentThread().getName() + "线程取到的值为:" + v);
                System.out.println(Thread.currentThread().getName() + "线程执行结束");
            }
        }).start();
        // 等待所有线程执行结束
        Thread.sleep(3000L);
        v = THREAD_LOCAL.get();
        System.out.println("Thread-0线程执行之后," + Thread.currentThread().getName() + "线程取到的值:" + v);
    }
}

执行结果:

Thread-0线程执行之前,main线程取到的值:mainthreadvalue
Thread-0线程取到的值:null
重新设置之后,Thread-0线程取到的值为:inThreadValue
Thread-0线程执行结束
Thread-0线程执行之后,main线程取到的值:mainthreadvalue

面试指南


面试官:那ThreadLocal中弱引用导致的内存泄漏是如何发生的?

小白:如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。

面试官:如何解决?

小白:查看源码会发现,ThreadLocal的get、set和remove方法都实现了对所有key为null的value的清除,但仍可能会发生内存泄露,因为可能使用了ThreadLocal的get或set方法后发生GC,此后不调用get、set或remove方法,为null的value就不会被清除。解决办法是每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

新网银行内推


base: 成都高新区, 新网银行,待遇从优,社招,校招均可,欢迎投递


相关文章
|
4月前
|
机器学习/深度学习 运维 安全
MyEMS开源能源管理系统简介
MyEMS 是一款基于 Python 和 React 的开源能源管理系统,采用 MIT 协议发布,适用于建筑、工厂、商场等多种场景。系统支持电、水、气等能源数据实时采集,兼容新能源设备接入,并提供智能分析、设备管理、节能优化等功能,助力企业降低能耗与运维成本,提升能源使用效率。
160 0
|
3月前
|
人工智能 数据可视化 API
2025大语言模型部署实战指南:从个人笔记本到企业级服务的全栈方案
本文深度解析四大主流大模型部署框架,覆盖个人开发、边缘设备与高并发生产场景。从Ollama、llama.cpp到vLLM与LM Studio,助你精准匹配技术方案,实现高效部署。
|
10月前
|
搜索推荐 安全 数据挖掘
深度解析:销售易CRM、神州云动CRM与纷享销客CRM的品牌特色与核心优势
销售易CRM是销售易公司推出的一款专注于提升销售效率和客户满意度的客户关系管理软件。成立于2011年,总部位于北京,是国内领先的企业级CRM服务商之一。销售易CRM以“连接客户、赋能销售”为使命,提供移动化办公、全流程管理、数据分析与洞察及集成与扩展等核心功能,助力企业实现销售流程的数字化转型和客户关系的精细化管理。 神州云动CRM隶属于神州数码集团,提供全面的CRM解决方案,特别适合大中型企业。依托集团强大的技术实力,神州云动CRM具备多行业适配、智能数据分析、灵活定制化和稳定可靠的云服务等优势,帮助企业构建高效的客户管理体系,实现客户价值的最大化。
|
网络协议 JavaScript 数据安全/隐私保护
深入浅出:使用WebSocket打造实时Web应用
【10月更文挑战第2天】本文介绍了WebSocket的基本概念、工作原理以及如何在项目中实现WebSocket通信。希望通过本文,读者能够掌握WebSocket,并考虑在自己的项目中实现实时功能。
|
负载均衡 Java Spring
Spring cloud gateway 如何在路由时进行负载均衡
Spring cloud gateway 如何在路由时进行负载均衡
2037 15
|
边缘计算 Cloud Native
“论SOA在企业集成架构设计中的应用”必过范文,突击2024软考高项论文
SOA架构,即面向服务的架构,它将系统中的所有功能都拆分为一个个独立的服务单元。这些服务通过相互间的沟通与配合,共同完成了整体业务逻辑的运作。在SOA架构中有几个核心概念:服务提供者、服务使用者、服务注册中心、服务规范、服务合同,这些概念清晰地阐述了服务应如何被提
474 6
“论SOA在企业集成架构设计中的应用”必过范文,突击2024软考高项论文
|
SQL 运维 分布式计算
StreamX 在联通数科万亿级实时计算中的生产实践
摘要: 本文源自 StreamX 在联通数科实时计算服务中的生产实践,作者是联通数科大数据负责人穆纯进及其团队,主要内容为: 1. 联通数科实时计算能力要求 2. 实时计算面临的挑战 3. StreamX 在联通数科的深度实践 4. 借助开源、反哺开源
268 0
|
人工智能 编解码 算法
使用LabVIEW AI视觉工具包快速实现霍夫圆和霍夫直线检测(含源码)
使用LabVIEW AI视觉工具包快速实现霍夫圆和霍夫直线检测(含源码)
446 0
|
Java 数据库连接 mybatis
mybatis与spring整合版本对应关系参考
mybatis与spring整合版本对应关系参考
3659 0
|
域名解析 Web App开发 缓存
教懂你什么是 “ 流量劫持 ”
还在等什么,快来一起讨论关注吧,公众号【八点半技术站】,欢迎加入社群
教懂你什么是 “ 流量劫持 ”