首页
题库
公司真题
专项练习
面试题库
在线编程
面试
面试经验
AI 模拟面试
简历
求职
学习
基础学习课
实战项目课
求职辅导课
专栏&文章
竞赛
我要招人
发布职位
发布职位、邀约牛人
更多企业解决方案
AI面试、笔试、校招、雇品
HR免费试用AI面试
最新面试提效必备
登录
/
注册
miximixi
东南大学 无线通信工程师
发布于江苏
关注
已关注
取消关注
@程序员打工人:
ThreadLocal原理及使用场景
ThreadLocal ThreadLocal意为线程本地变量,用于解决多线程并发时访问共享变量的问题。 所谓的共享变量指的是在堆中的实例、静态属性和数组;对于共享数据的访问受Java的内存模型(JMM)的控制,其模型如下: 每个线程都会有属于自己的本地内存,在堆(也就是上图的主内存)中的变量在被线程使用的时候会被复制一个副本线程的本地内存中,当线程修改了共享变量之后就会通过JMM管理控制写会到主内存中。 很明显,在多线程的场景下,当有多个线程对共享变量进行修改的时候,就会出现线程安全问题,即数据不一致问题。常用的解决方法是对访问共享变量的代码加锁(synchronized或者Lock)。但是这种方式对性能的耗费比较大。在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题。 当然锁和ThreadLocal使用场景还是有区别的,具体区别如下: 一、ThreadLocal的使用及原理 1.1 使用 一般都会将ThreadLocal声明成一个静态字段,同时初始化如下: static ThreadLocal<Object> threadLocal = new ThreadLocal<>(); 其中Object就是原本堆***享变量的数据。 例如,有个User对象需要在不同线程之间进行隔离访问,可以定义ThreadLocal如下: public class Test { static ThreadLocal<User> threadLocal = new ThreadLocal<>();} 常用的方法 set(T value):设置线程本地变量的内容。 get():获取线程本地变量的内容。 remove():移除线程本地变量。注意在线程池的线程复用场景中在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。 public class Test { static ThreadLocal<User> threadLocal = new ThreadLocal<>(); public void m1(User user) { threadLocal.set(user); } public void m2() { User user = threadLocal.get(); // 使用 // 使用完清除 threadLocal.remove(); }} 1.2 原理 那么如何究竟是如何实现在每个线程里面保存一份单独的本地变量呢?首先,在Java中的线程是什么呢?是的,就是一个Thread类的实例对象!而一个实例对象中实例成员字段的内容肯定是这个对象独有的,所以我们也可以将保存ThreadLocal线程本地变量作为一个Thread类的成员字段,这个成员字段就是: /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; 是一个在ThreadLocal中定义的Map对象,保存了该线程中的所有本地变量。ThreadLocalMap中的Entry的定义如下: static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; // key为一个ThreadLocal对象,v就是我们要在线程之间隔离的对象 Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }} ThreadLocalMap和Entry都在ThreadLocal中定义。 ThreadLocal::set方法的原理 set方法的源码如下: public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的threadLocals字段 ThreadLocalMap map = getMap(t); // 判断线程的threadLocals是否初始化了 if (map != null) { map.set(this, value); } else { // 没有则创建一个ThreadLocalMap对象进行初始化 createMap(t, value); }} createMap方法的源码如下: void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);} map.set方法的源码如下: /*** 往map中设置ThreadLocal的关联关系* set中没有使用像get方法中的快速选择的方法,因为在set中创建新条目和替换旧条目的内容一样常见,* 在替换的情况下快速路径通常会失败(对官方注释的翻译)*/ private void set(ThreadLocal<?> key, Object value) { // map中就是使用Entry[]数据保留所有的entry实例 Entry[] tab = table; int len = tab.length; // 返回下一个哈希码,哈希码的产生过程与神奇的0x61c88647的数字有关 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();} Thread::get方法的原理 public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { // 获取ThreadLocal对应保留在Map中的Entry对象 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") // 获取ThreadLocal对象对应的值 T result = (T)e.value; return result; } } // map还没有初始化时创建map对象,并设置null,同时返回null return setInitialValue();} ThreadLocal::remove()方法原理 public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); // 键在直接移除 if (m != null) { m.remove(this); }} ThreadLocalMap的类结构体系如下: 1.3 ThreadLocal设计 在JDK早期的设计中,每个ThreadLocal都有一个map对象,将线程作为map对象的key,要存储的变量作为map的value,但是现在已经不是这样了。 JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量,是泛型,其具体过程如下: 每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals); Map里面存储ThreadLocal对象(key)和线程的变量副本(value); Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值; 对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。 JDK8之后设计的好处在于: 每个Map存储的Entry的数量变少,在实际开发过程中,ThreadLocal的数量往往要少于Thread的数量,Entry的数量减少就可以减少哈希冲突。 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用,早期的ThreadLocal并不会自动销毁。 使用ThreadLocal的好处 保存每个线程绑定的数据,在需要的地方可以直接获取,避免直接传递参数带来的代码耦合问题; 各个线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。 二、ThreadLocal内存泄露问题 内存泄露问题:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄露堆积将会导致内存溢出。 ThreadLocal的内存泄露问题一般考虑和Entry对象有关,在上面的Entry定义可以看出ThreadLocal::Entry被弱引用所修饰。JVM会将弱引用修饰的对象在下次垃圾回收中清除掉。这样就可以实现ThreadLocal的生命周期和线程的生命周期解绑。但实际上并不是使用了弱引用就A会发生内存泄露问题,考虑下面几个过程: 使用强引用 当ThreadLocal Ref被回收了,由于在Entry使用的是强引用,在Current Thread还存在的情况下就存在着到达Entry的引用链,无法清除掉ThreadLocal的内容,同时Entry的value也同样会被保留;也就是说就算使用了强引用仍然会出现内存泄露问题。 使用弱引用 当ThreadLocal Ref被回收了,由于在Entry使用的是弱引用,因此在下次垃圾回收的时候就会将ThreadLocal对象清除,这个时候Entry中的KEY=null。但是由于ThreadLocalMap中任然存在Current Thread Ref这个强引用,因此Entry中value的值任然无法清除。还是存在内存泄露的问题。 由此可以发现,使用ThreadLocal造成内存泄露的问题是因为:ThreadLocalMap的生命周期与Thread一致,如果不手动清除掉Entry对象的话就可能会造成内存泄露问题。因此,需要我们在每次在使用完之后需要手动的remove掉Entry对象。 那么为什么使用弱引用? 避免内存泄露的两种方式:使用完ThreadLocal,调用其remove方法删除对应的Entry或者使用完ThreadLocal,当前Thread也随之运行结束。第二种方法在使用线程池技术时是不可以实现的。 所以一般都是自己手动调用remove方法,调用remove方法弱引用和强引用都不会产生内存泄露问题,使用弱引用的原因如下: 在ThreadLocalMap的set/getEntry中,会对key进行判断,如果key为null,那么value也会被设置为null,这样即使在忘记调用了remove方法,当ThreadLocal被销毁时,对应value的内容也会被清空。多一层保障! 总结:存在内存泄露的有两个地方:ThreadLocal和Entry中Value;最保险还是要注意要自己及时调用remove方法!!! 三、ThreadLocal的应用场景 场景一:在重入方法中替代参数的显式传递 假如在我们的业务方法中需要调用其他方法,同时其他方法都需要用到同一个对象时,可以使用ThreadLocal替代参数的传递或者static静态全局变量。这是因为使用参数传递造成代码的耦合度高,使用静态全局变量在多线程环境下不安全。当该对象用ThreadLocal包装过后,就可以保证在该线程中独此一份,同时和其他线程隔离。 例如在Spring的@Transaction事务声明的注解中就使用ThreadLocal保存了当前的Connection对象,避免在本次调用的不同方法中使用不同的Connection对象。 场景二:全局存储用户信息 可以尝试使用ThreadLocal替代Session的使用,当用户要访问需要授权的接口的时候,可以现在拦截器中将用户的Token存入ThreadLocal中;之后在本次访问中任何需要用户用户信息的都可以直接冲ThreadLocal中拿取数据。例如自定义获取用户信息的类AuthHolder: public class AuthNHolder { private static final ThreadLocal<Map<String,String>> threadLocal = new ThreadLocal<>(); public static void map(Map<String,String> map){ threadLocal.set(map); } // 获取用户id public static String userId(){ return get("userId"); } // 根据键值获取对应的信息 public static String get(String key){ Map<String,String> map = getMap(); return map.get(key); } // 用完清空ThreadLocal public static void clear(){ threadLocal.remove(); }} ThreadLocal里面封装的value只是一个例子,根据具体业务需求改就行了。 场景三:解决线程安全问题 依赖于ThreadLocal本身的特性,对于需要进行线程隔离的变量可以使用ThreadLocal进行封装。 四、总结 ThreadLocal更像是对其他类型变量的一层包装,通过ThreadLocal的包装使得该变量可以在线程之间隔离和当前线程全局共享。 线程的隔离性和变量的线程全局共享性得益于在每个Thread类中的threadlocals字段。(从类实例对象的角度抽象的去看Java中的线程!!!) ThreadLocalMap中Entry的Key不管是否使用弱引用都有内存泄露的可能。引起内存泄露主要在于ThreadLocal对象和Entry中的Value对象,因此要确保每次使用完之后都remove掉Entry!
点赞 2
评论 1
全部评论
推荐
最新
楼层
暂无评论,快来抢首评~
相关推荐
10-08 15:39
游卡_游戏客户端开发(准入职员工)
游卡内推,游卡内推码
游卡服务端开发面经(已oc) 一面1.自我介绍2.虚函数和多态3.vector删除一个元素如何实现的,讲讲移动语义,如何回收一个vector的内存(创建一个空的,移动给现在的(挺巧妙))4.讲讲几种智能指针的应用场景、weak_ptr如何保证在使用期间资源不失效的5.socket编程的流程6.进程、线程、协程7.cpu计算密集型任务用多线程还是多协程,为什么8.死锁是什么,如何解决9.每次生成1个1到1亿的随机数、且不重复10.反问二面+hr面1.自我介绍2.实验室项目拷打,做的东西偏底层,为什么想来做游戏3.bustub,为什么用B+树4.了解innodb的页面组织形式吗5.了解mangod...
点赞
评论
收藏
分享
10-09 23:45
东南大学 C++
途游一面 C++
体验最好的一集,全程像打怪闯关,层层推进,整体偏基础。C++:1.int取值范围?2^32 + 1 = ?(int情况下)2.空类占几个字节?class A{ int a, char b} 占几个字节?3.析构函数为什么最好是虚函数?5.虚析构函数调用的过程(其实问的是虚函数实现多态的原理)6.Base * a= nullptr; a可以调用虚函数吗?a可以调用成员函数吗?那a直接调用成员函数肯定会有问题,什么情况下有问题?7.C++中,在main函数执行的代码有哪些?那全局变量的初始化顺序是怎么样的?排序:8.什么是稳定/不稳定排序?9.假如要对100个玩家,按照等级排序,那么希望调用一次排...
查看10道真题和解析
点赞
评论
收藏
分享
08-19 19:57
石河子大学 C++
家人们,能找到工作吗,投了一阵没反应,
企鹅百度字节的孝子:
为啥本科只有两年啊
校招求职吐槽
点赞
评论
收藏
分享
09-21 23:16
门头沟学院 Java
把公司当家
虽然他说的很难听,但是真的有点扎心啊😭
传奇逃兵王:
招不起就别招,叽里咕噜说啥呢
点赞
评论
收藏
分享
10-10 13:11
正浩创新EcoFlow_电力电子软件工程师(准入职员工)
正浩创新内推,正浩创新内推码
一整个面试下来,感觉正浩的技术面真的很扎实!趁热乎整理一波面经(纯干货版),希望能帮到准备面试的学弟学妹们~ 【结尾有血泪总结建议!】🔧 【一面 · 硬核技术面】面试官问的非常细致深入🔥,主要围绕电源知识,我总结出的题目供参考:1为什么相同载波频率下,单相全桥用单极性倍频比双极性调制,变压器发热少?2为什么光靠软件死区不够可靠?(死区门道多啊!)3MOSFET vs IGBT,优缺点怎么选?(器件基础)4LCL滤波器咋设计?(公式忘了,幸好说清了关键参数影响😅)5LC vs LCL滤波器区别?6逆变器输出电流THD从3%降到2%以下,你会改啥?7Boost拓扑和输入输出关系?(基础中的基...
点赞
评论
收藏
分享
评论
点赞成功,聊一聊 >
点赞
收藏
分享
评论
提到的真题
返回内容
全站热榜
更多
1
...
HR面,到底该准备些啥(附核心问题回答思路)
1.6W
2
...
除了卷大厂,还有其他出路吗。。。
3874
3
...
懂车帝二面 2025.10.11 1h32min
3759
4
...
双非秋招timeline供参考(腾讯字节阿里快手美团)
2830
5
...
小红书一面面经
2570
6
...
牛牛求救🆘,不敢梭哈后端第二技能点怎么搭配
2327
7
...
10.12pdd笔试大鸭蛋
2256
8
...
10.12 拼多多技术岗笔试 第二题 求教
2120
9
...
第一次去北京那么远的地方实习,心里总是不安,大家会有这种感觉吗?
2010
10
...
华为10月10号考的手写LSTM被压中了
1773
创作者周榜
更多
正在热议
更多
#
面包vs爱情,怎么选?
#
7642次浏览
89人参与
#
职场新人体验
#
83822次浏览
595人参与
#
深信服秋招来了
#
279668次浏览
2915人参与
#
实习生如何通过转正
#
104179次浏览
1394人参与
#
tplink提前批进度交流
#
207018次浏览
1506人参与
#
安克创新求职进展汇总
#
53875次浏览
528人参与
#
爱玛科技集团求职进展汇总
#
27111次浏览
195人参与
#
Tplink求职进展汇总
#
180339次浏览
912人参与
#
秋招结束之后的日子
#
86190次浏览
976人参与
#
面试被问“你的缺点是什么?”怎么答
#
154643次浏览
2146人参与
#
贝壳求职进展汇总
#
34511次浏览
184人参与
#
硬件/芯片公司岗位评价
#
8306次浏览
28人参与
#
Offer比较,你最看重什么?
#
215194次浏览
1389人参与
#
互联网公司爆料
#
144645次浏览
708人参与
#
招银网络求职进展汇总
#
168316次浏览
992人参与
#
联影求职进展汇总
#
43013次浏览
284人参与
#
华为海思工作体验
#
29045次浏览
120人参与
#
新凯来求职进展汇总
#
49707次浏览
126人参与
#
材料进Fab厂真的劝退吗?
#
56082次浏览
204人参与
#
五一之后,实习真的很难找吗?
#
88002次浏览
556人参与
#
应届生,你找到工作了吗
#
68989次浏览
459人参与
#
总结:哪家公司最喜欢泡池子
#
144026次浏览
520人参与
牛客网
牛客网在线编程
牛客网题解
牛客企业服务