文章目录

  • ThreadLocal
    • 1、基本认识
    • 2、内部结构
    • 3、ThreadLocal常用方法
    • 4、ThreadLocalMap的源码解析
    • 5、内存泄漏

ThreadLocal

1、基本认识

作用:提供线程内的局部变量,不同的线程之间互不干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间的一些公共变量传递的复杂度。

浅拿生活中的例子理解一波:

  • 每个房子就是每个线程,每个房子中的清洁工具为对应的房子独有,清洁工具就是ThreadLocalMap
  • 每个房子的清洁工具的使用互不干扰
  • 清洁工具含有扫把,拖把等,扫把和拖把是不同的Threadlocal对象,ThreadLocalMap存储的记录entry中的键key是Threadlocal对象引用,各个房子的清洁工具可以适用在房子的各个楼层,各个楼层即不同组件。

总结:多线程并发情况下,线程隔离,每个线程的变量都是独立的,不会互相影响,通过Threadlocal对象在同一线程不同组件间传递公共变量。

2、内部结构

图源B站视频

好处:

  • 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用

解读 JDK 8 结构:

  • Thread维护一个ThreadLocalMap,ThreadLocalMap内部是一个Entry数组,一个Entry包含key 和 value,key 为不同的ThreadLocal对象(注:泛型ThreadLocal<?>)

3、ThreadLocal常用方法

public T get() {Thread t = Thread.currentThread();//获取当前线程ThreadLocalMap map = getMap(t);//获取当前线程所维护的ThreadLocalMap//注:每个线程维护自己的ThreadLocalMap 相当于我前文例子的清洁工具if (map != null) {//获取当前ThreadLocal的记录ThreadLocalMap.Entry e = map.getEntry(this);//this,即调用这个方法的ThreadLocalif (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;//得到ThreadLocal的value值return result;}}/* 有两种情况会执行到这里1、当前线程的ThreadLocalMap还未创建2、没有该ThreadLocal的entry记录*/return setInitialValue();
}
private T setInitialValue() {T value = initialValue();//默认返回null,子类可以重写该方法Thread t = Thread.currentThread();//获取当前线程ThreadLocalMap map = getMap(t);//获取当前线程所维护的ThreadLocalMapif (map != null)//没有该ThreadLocal的entry记录map.set(this, value);//增加该记录elsecreateMap(t, value);//当前线程的ThreadLocalMap还未创建return value;
}public void set(T value) {Thread t = Thread.currentThread();//获取当前线程ThreadLocalMap map = getMap(t);//获取当前线程所维护的ThreadLocalMapif (map != null)map.set(this, value);//增加该记录elsecreateMap(t, value);//当前线程的ThreadLocalMap还未创建
}
//移除map中的某个entry
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());//获取当前线程的ThreadLocalMapif (m != null)m.remove(this);//删除 key 值为 调用该方法的ThreadLocal 的记录
}

总结:

  • 第一次调用set()get() 方法时都可以创建ThreadLocalMap,初始值不同,get()的初始值默认为null(子类可重写获取初始值的方法initialValue()

4、ThreadLocalMap的源码解析

ThreadLocalMapThreadLocal中的内部类,是独立实现的类似于HashMap的类,并没有实现Map接口

static class ThreadLocalMap {/*** entry 继承弱引用, 即key 值 存储的是ThreadLocal弱引用对象*/static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}/*** entry数组的初始化长度,与hashmap一样 必须是2的幂次方*/private static final int INITIAL_CAPACITY = 16;/*** entry数组*/private Entry[] table;/*** 数组存储的记录个数*/private int size = 0;/*** 扩容的阈值,超过该值则进行扩容,此时默认为0*/private int threshold; /*** 扩容的阈值的设置*/private void setThreshold(int len) {threshold = len * 2 / 3;}/*** 像一个环一样,从索引下标 i 走到末尾,到达末尾之后下一步走 0 下标*/private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}/*** 像一个环一样,从索引下标 i 走到0,到达0之后下一步走 len - 1 下标*/private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);}/** * 创建一个map,当有记录需插入时,数组才初始化*/ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}/*** 该方法是在 createInheritedMap 时调用* 创建一个map 包含所有从父线程继承的ThreadLocal记录* parentMap 父线程的ThreadLocalMap*/private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {//深拷贝@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}}/*** 获取entry*/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 //e == null 或者 当前索引对应的记录 key值 不等于 keyreturn getEntryAfterMiss(key, i, e);}private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;while (e != null) {//因为 当前索引对应的记录 key值 不等于 key 调用的该方法ThreadLocal<?> k = e.get();if (k == key)//找到该记录return e;if (k == null) expungeStaleEntry(i);//置该记录为null 致使下次垃圾回收时回收该空间elsei = nextIndex(i, len);//遍历数组e = tab[i];}return null;// 1、因为 e == null 调用该方法 即说明该key值对应的记录 不存在// 2、遍历完数组 key值对应的记录不存在}private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;//ThreadLocalMap内的entry数组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();//得到entry的key值ThreadLocalif (k == key) {//相等,说明ThreadLocal的记录已存在e.value = value;//更新return;}if (k == null) {//检索过程,发现有key值为null的(对应的ThreadLocal已被垃圾回收),替换记录replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);//e == null 退出循环或者当前下标的数组位置不为空int sz = ++size;//记录数 + 1//cleanSomeSlots(i, sz) 这个过程清除数组中key值为null的记录if (!cleanSomeSlots(i, sz) && sz >= threshold)//数组中没有key值为null的记录且超过扩容阈值rehash();//扩容}}    /*** 删除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) {e.clear();expungeStaleEntry(i);return;}}}private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;tab[staleSlot].value = null;tab[staleSlot] = null;size--;// 将i下标后的所有记录重新hashEntry e;int i;//这个过程就是在查看是否还有null值//以及重新定位一些因为哈希冲突而不在k.threadLocalHashCode & (len - 1)下标的记录//因为在获取entry时,就是通过key值得到得索引一直向右遍历找到,如果不重新定位,可能就造成,实际上该记录存在,但最终没查找到for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null;tab[i] = null;size--;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {//说明 原本key得到的索引 造成哈希冲突tab[i] = null;while (tab[h] != null)//重新定位该key值h = nextIndex(h, len);tab[h] = e;}}}return i;}private boolean cleanSomeSlots(int i, int n) {//是否存有key为null的记录boolean removed = false;Entry[] tab = table;int len = tab.length;do {i = nextIndex(i, len);Entry e = tab[i];if (e != null && e.get() == null) {n = len;removed = true;i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);return removed;}private void rehash() {expungeStaleEntries();if (size >= threshold - threshold / 4) //数组初始长度为16时, size >= 8时resize();//扩容}/*** 扩容为原来数组的两倍*/private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;int newLen = oldLen * 2;Entry[] newTab = new Entry[newLen];int count = 0;for (int j = 0; j < oldLen; ++j) {Entry e = oldTab[j];if (e != null) {ThreadLocal<?> k = e.get();if (k == null) {e.value = null; //  GC 回收} else {int h = k.threadLocalHashCode & (newLen - 1);while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;count++;}}}setThreshold(newLen);size = count;table = newTab;}/*** 删除所有 key 为 null 的记录*/private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;for (int j = 0; j < len; j++) {Entry e = tab[j];if (e != null && e.get() == null)expungeStaleEntry(j);}}
}

5、内存泄漏

内存泄漏的概念:指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的堆积终将导致内存溢出。

  • 如果key使用强引用
    假设业务代码中使用完ThreadLocal,那么ThreadLocal的引用对象被回收了
  • 但是ThreadLocalMap强引用了ThreadLocal造成ThreadLocal无法被回收
  • 在没有手动删除这个entry的情况下,始终有当前线程指向ThreadLocalMap,>entry就不会被回收,entry包含ThreadLocal实例和value,导致entry内存泄漏。
    也就是说key使用了强引用是无法完全避免内存泄露
  • 如果key使用弱引用
    同样假设业务代码中使用完ThreadLocal,那么ThreadLocal的引用被回收了
  • 由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向ThreadLocal实例,所以ThreadLocal被垃圾回收,此时entry中的key = null
  • 在没有手动删除这个entry的情况下,始终有当前线程指向ThreadLocalMap,由于key = null,那么对应的value就永远都不会被访问到,导致value内存泄漏。

比较以上两种情况,内存泄漏的发生跟ThreadLocalMap中的 key是否使用弱引用是没有关系的
在以上两种内存泄漏的情况中,都有两个前提:

  1. 没有手动删除这个Entry
  2. CurrentThread依然运行

第一点:只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。
第二点:由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。

在JDK 8 中是由线程来维护ThreadLocalMap,因此ThreadLocalMap的生命周期跟Thread一样长,如果没有手动remove方法就会导致内存泄漏。

总结: 实际上内存泄漏与key是强引用还是弱引用无关,弱引用反而比强引用多一层保障。ThreadLocalMap内部会通过key 是否等于null来检验,若是则置value等于null,防止用户未调用remove方法移除entry。

ThreadLocal相关推荐

  1. FastThreadLocal吞吐量居然是ThreadLocal的3倍

    目前关于FastThreadLocal的很多文章都有点老有点过时了(本文将澄清几个误区),很多文章关于FastThreadLocal介绍的也不全,希望本篇文章可以带你彻底理解FastThreadLoc ...

  2. 正确理解ThreadLocal

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt107 首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的, ...

  3. 什么是ThreadLocal

    顾名思义它是local variable(线程局部变量).它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突.从线 ...

  4. 为什么jdk源码推荐ThreadLocal使用static

    ThreadLocal是线程私有变量,本身是解决多线程环境线程安全,可以说单线程实际上没必要使用. 既然多线程环境本身不使用static,那么又怎么会线程不安全.所以这个问题本身并不是问题,只是有人没 ...

  5. Spring源码分析【6】-ThreadLocal的使用和源码分析

    Spring代码使用到了ThreadLocal java.lang.ThreadLocal.set getMap java.lang.Thread.threadLocals定义 回到set 如果map ...

  6. threadlocal使用场景_深入剖析ThreadLocal

    点击上方 IT牧场 ,选择 置顶或者星标 技术干货每日送达 朋友们在遇到线程安全问题的时候,大多数情况下可能会使用synchronized关键字,每次只允许一个线程进入锁定的方法或代码块,这样就可以保 ...

  7. ThreadLocal源码分析

    ThreadLocal的作用 Java对象是线程间共享的,但有时我们需要一些线程间隔离的对象,该对象只能由同一个线程读写,对其他线程不可见.ThreadLocal正式提供了这样的机制,详细使用方式请参 ...

  8. ThreadLocal的使用场景

    最近项目中遇到如下的场景:在执行数据迁移时,需要按照用户粒度加锁,因此考虑使用排他锁,迁移工具和业务服务属于两个服务,因此需要使用分布式锁. 我们使用缓存(Tair或者Redis)实现分布式锁,具体代 ...

  9. ThreadLocal的使用方法

    ThreadLocal的含义是Thread Local Variable,它可以声明一个字段,使得不同的线程访问这个字段时,获取的都是不同的副本,互不影响. ThreadLocal的作用和在每个Thr ...

  10. PageHelper 使用 ThreadLocal 的线程复用问题

    欢迎关注方志朋的博客,回复"666"获面试宝典 来源:https://blog.csdn.net/qq_38245668/article/details/105984171/ 前言 ...

最新文章

  1. SugarCRM 主表-自定义字段
  2. 【C++】 为什么C++空类占一个字节
  3. C#-修改图书借阅管理系统-错误与SQL server 2008错误、复制数据库
  4. 1024程序猿节:致敬改变世界的你
  5. XunSearch的使用
  6. NET sturct值类型
  7. BZOJ2424 [HAOI2010]订货
  8. ASSERT(0) 详解
  9. 9、网络详解 学习笔记
  10. 34个漂亮的应用程序后台管理界面设计(系列三)
  11. android真机测试什么不同,android真机测试闪退
  12. pojnbsp;2392nbsp;Spacenbsp;Elevatornbsp;背包
  13. python用来占位的关键字_python-study/Readme.md at master · wchhuangya/python-study · GitHub
  14. b站网页版倍速无效_看网课讲师太啰嗦太慢?在线视频课程效率低?教你自定义超倍速看
  15. 通过iptable进行流量转发
  16. UDT的连接建立和释放
  17. PSTN与VoIP相关知识
  18. 一套完整的动环监控系统,适用于各类机房、学校机房、医院机房、银行库房等
  19. 一名合格的管理者应具备哪些能力与素质?
  20. 应用计算机测定线性电阻,实验31-应用计算机测电阻伏安特性

热门文章

  1. Windows Server 2008 R2远程桌面服务配置和授权激活
  2. 总结:Oracle 递归查询
  3. 统计软件SAS入门教程:SAS程序初步
  4. 【C++课程设计】基于单向链表的通讯录管理程序
  5. w ndows7手机桌面,Windows 7中有哪些常用的桌面小工具
  6. oceanbase ODC和Obclient连接mysql类型的ob库
  7. 重编译 microsip 和 pjsip 支持 mp3 录音
  8. c语言小车程序,循迹小车程序C语言
  9. esp32的MQTT物联网开发记录
  10. Lubuntu安装屏幕键盘onboard,使触摸屏可以登录和输入