ThreadLocal原理介绍及应用场景
本次给大家介绍重要的工具threadlocal
。讲解内容如下,同时介绍什么场景下发生内存泄漏,如何复现内存泄漏,如何正确使用它来避免内存泄漏。
-
threadlocal
是什么?有哪些用途? -
threadlocal
如何使用 -
threadlocal
原理 -
threadlocal
使用有哪些坑及注意事项
1. threadlocal是什么?有哪些用途?
首先介绍thread
类中属性threadlocals:
/* threadlocal values pertaining to this thread. this map is maintained * by the threadlocal class. */
threadlocal.threadlocalmap threadlocals = null;
我们发现thread并没有提供成员变量threadlocals的设置与访问的方法,那么每个线程的实例threadlocals参数我们如何操作呢?这时我们的主角:threadlocal就登场了。
所以有那么一句总结:threadlocal
是线程thread中
属性threadlocals的管理者。
也就是说我们对于threadlocal
的get, set,remove的操作结果都是针对当前线程thread实例的threadlocals存,取,删除操作。类似于一个开发者的任务,产品经理左右不了,产品经理只能通过技术leader来给开发者分配任务。下面再举个栗子,进一步说明他们之间的关系:
1.每个人都一张银行卡
2.每个人每张卡都有一定的余额。
3.每个人获取银行卡余额都必须通过该银行的管理系统。
4.每个人都只能获取自己卡持有的余额信息,他人的不可访问。
映射到我们要说的threadlocal
- 1.card类似于thread
- 2.card余额属性,卡号属性等类似于treadlocal内部属性集合threadlocals
- 3.cardmanager类似于threadlocal管理类
那threadlocal有哪些应用场景呢?
其实我们无意间已经时时刻刻在使用threadlocal提供的便利,如果说多数据源的切换你比较陌生,那么spring提供的声明式事务就再熟悉不过了,我们在研发过程中无时无刻不在使用,而spring声明式事务的重要实现基础就是threadlocal,只不过大家没有去深入研究spring声明式事务的实现机制。后面有机会我会给大家介绍spring声明式事务的原理及实现机制。
原来threadlocal
这么强大,但应用开发者使用较少,同时有些研发人员对于threadlocal
内存泄漏,等潜在问题,不敢试用,恐怕这是对于threadlocal
最大的误解,后面我们将会仔细分析,只要按照正确使用方式,就没什么问题。如果threadlocal存在问题,岂不是spring声明式事务是我们程序最大的潜在危险吗?
2.threadlocal如何使用
为了更直观的体会threadlocal
的使用我们假设如下场景
- 1.我们给每个线程生成一个id。
- 2.一旦设置,线程生命周期内不可变化。
- 3.容器活动期间不可以生成重复的id
我们创建一个threadlocal管理类:
测试程序如下:我们同一个线程不断get,测试id是否变化,同时测试完成后我们就将其释放掉。
在主程序中我们开启多个线程测试不通线程之间是否会影响
不出意外我们的结果为:
结果:确实是不同线程间id不同,相同线程id相同。
3.threadlocal原理
①threadlocal类结构及方法解析:
上图可知:threadlocal
三个方法get, set , remove以及内部类`threadlocalmap
②threadlocal及thread之间的关系:
从这张图我们可以直观的看到thread中属性threadlocals,作为一个特殊的map,它的key值就是我们threadlocal
实例,而value值这是我们设置的值。
③threadlocal的操作过程:
我们以get方法为例:
其中getmap(t)返回的就上当前线程的threadlocals,如下图,然后根据当前threadlocal实例对象作为key获取threadlocalmap中的value,如果首次进来这调用setinitialvalue()
set的过程也类似:
注意:threadlocal
中可以直接t.threadlocals
是因为thread
与threadlocal
在同一个包下,同样thread可以直接访问threadlocal.threadlocalmap threadlocals = null;
来进行声明属性。
4.threadlocal使用有哪些坑及注意事项
我经常在网上看到骇人听闻的标题,threadlocal
导致内存泄漏,这通常让一些刚开始对threadlocal
理解不透彻的开发者,不敢贸然使用。越不用,越陌生。这样就让我们错失了更好的实现方案,所以敢于引入新技术,敢于踩坑,才能不断进步。
我们来看下为什么说threadlocal
会引起内存泄漏,什么场景下会导致内存泄漏?
先回顾下什么叫内存泄漏,对应的什么叫内存溢出
- ①memory overflow:内存溢出,没有足够的内存提供申请者使用。
- ②memory leak:内存泄漏,程序申请内存后,无法释放已申请的内存空间,内存泄漏的堆积终将导致内存溢出。
显然是treadlocal在不规范使用的情况下导致了内存没有释放。
红框里我们看到了一个特殊的类weakreference
,同样这个类,应用开发者也同样很少使用,这里简单介绍下吧
类型 | 回收时间 | 应用场景 |
---|---|---|
强引用 | 一直存活,除非gc roots不可达 | 所有程序的场景,基本对象,自定义对象等 |
软引用 | 内存不足时会被回收 | 一般用在对内存非常敏感的资源上,用作缓存的场景比较多,例如:网页缓存、图片缓存 |
弱引用 | 只能存活到下一次gc前 | 生命周期很短的对象,例如threadlocal中的key。 |
虚引用 | 随时会被回收, 创建了可能很快就会被回收 | 可能被jvm团队内部用来跟踪jvm的垃圾回收活动 |
既然weakreference
在下一次gc即将被回收,那么我们的程序为什么没有出问题呢?
- ①所以我们测试下弱引用的回收机制:
这一种存在强引用不会被回收。
这里没有强引用将会被回收。
上面演示了弱引用的回收情况,下面我们看下threadlocal的弱引用回收情况。
- ②
threadlocal
的弱引用回收情况
如上图所示,我们在作为key的threadlocal对象没有外部强引用,下一次gc必将产生key值为null的数据,若线程没有及时结束必然出现,一条强引用链
threadref–>thread–>threadlocalmap–>entry,所以这将导致内存泄漏。
下面我们模拟复现threadlocal导致内存泄漏:
1.为了效果更佳明显我们将我们的treadlocals的存储值value设置为1万字符串的列表:
class threadlocalmemory { // thread local variable containing each thread's id public threadlocal<list<object>> threadid = new threadlocal<list<object>>() { @override protected list<object> initialvalue() { list<object> list = new arraylist<object>(); for (int i = 0; i < 10000; i++) { list.add(string.valueof(i)); } return list; } }; // returns the current thread's unique id, assigning it if necessary public list<object> get() { return threadid.get(); } // remove currentid public void remove() { threadid.remove(); } }
测试代码如下:
public static void main(string[] args) throws interruptedexception { // 为了复现key被回收的场景,我们使用临时变量 threadlocalmemory memeory = new threadlocalmemory(); // 调用 incrementsamethreadid(memeory); system.out.println("gc前:key:" + memeory.threadid); system.out.println("gc前:value-size:" + refelectthreadlocals(thread.currentthread())); // 设置为null,调用gc并不一定触发垃圾回收,但是可以通过java提供的一些工具进行手工触发gc回收。 memeory.threadid = null; system.gc(); system.out.println("gc后:key:" + memeory.threadid); system.out.println("gc后:value-size:" + refelectthreadlocals(thread.currentthread())); // 模拟线程一直运行 while (true) { } }
此时我们如何知道内存中存在memory leak呢?
我们可以借助jdk提供的一些命令dump当前堆内存,命令如下:
jmap -dump:live,format=b,file=heap.bin <pid>
然后我们借助mat可视化分析工具,来查看对内存,分析对象实例的存活状态:
首先打开我们工具提示我们的内存泄漏分析:
这里我们可以确定的是threadlocalmap实例的entry.value是没有被回收的。
最后我们要确定entry.key是否还在?打开dominator tree,搜索我们的threadlocalmemory,发现并没有存活的实例。
以上我们复现了threadlocal
不正当使用,引起的内存泄漏。demo在这里。
所以我们总结了使用threadlocal
时会发生内存泄漏的前提条件:
- ①
threadlocal
引用被设置为null,且后面没有set,get,remove操作。 - ②线程一直运行,不停止。(线程池)
- ③触发了垃圾回收。(minor gc或full gc)
我们看到threadlocal
出现内存泄漏条件还是很苛刻的,所以我们只要破坏其中一个条件就可以避免内存泄漏,单但为了更好的避免这种情况的发生我们使用threadlocal时遵守以下两个小原则:
-
①threadlocal申明为private static final。
private与final 尽可能不让他人修改变更引用,
static 表示为类属性,只有在程序结束才会被回收。 -
②threadlocal使用后务必调用remove方法。
最简单有效的方法是使用后将其移除。
到此这篇关于threadlocal原理介绍及应用场景的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: GraphQL实战篇(一)