ThreadLocal技术分享实录
前言
曾经读到一篇文章,有句话印象十分的深刻:没有一个内容作者,是可以凭空冒出的,你看见的所有看似普通的文字,背后往往都需要多年的积累才能酝酿。
的确如此,看似小小的threadlocal,往往随着年龄和阅历的增长,体会到的内涵是不一样的。因为背后是时间的沉淀和苦灯冷月的思考。
第一部分:首先说一下我学习threadlocal的经历。
刚开始,我也不是一下子就能把threadlocal理解透彻的,也是有个过程。我给大家分享一下我的学习过程。
我刚开始的理解思路,也是这样的:首先把它放在线程这个大的背景下去琢磨,然后认为它是解决多线程的变量冲突问题。
但是这样的思路并不对,效果不好。由于"线程"这个东西是抽象的东西,对于所有的人来说,当然也包括我自己,"线程"就是一只拦路虎,让每一个接触ThreadLocal的人都产生了内心的抵触,产生了虚无缥缈的无助感,所以大家对ThreadLocal的理解没有深度,没有灵性,很教条,很空洞。
后来,我做了一段时间的C/C++开发。C/C++这门语言有个特点,就是繁琐,语法规范特别的繁琐。举个例子来说,它里面把变量分为很多的类型。学习起来非常的费劲。不同类型的变量,对应不同的存储场景。
越是费劲的东西,往往印象深刻。后来,我再碰到Java里面的threadlocal,我就突然有个灵感,何不从变量的角度去理解它呢。从此之后,我有了一个完全新的认识。
为什么我能抛开线程而从变量的角度来认识和掌握 ThreadLocal,而其他的人都被困在"线程"的格局里面出不来呢?这就是知识的宽度决定了思考的深度。因为我除了Java熟悉以外,还熟悉C/C++。
我一直觉得,任何Java高手,他肯定不是只研究Java的,一定是多方面的人才。
别的不说,C/C++这门语言,我建议有时间的人,应该看看。你要想成为一个Java高手,就该如此。其实,大家都没有时间,没有时间也要学会砍掉一些投入产出比小的东西。
看看其他的语言,反过来对Java的理解会更深刻。一头扎在Java里面,思维往往形成惯性了,形成思维定势了。
java8里面的函数编程,放在java的背景下,它的语法规范与之前的java有很大的区别,非常的别拧和突兀。但是你搞一下scala,你会发现,java8的函数编程竟然如此顺畅。
上面是插了一点,下面继续threadlocal。
第二部分:我说一下什么是从变量的角度去理解threadlocal。
从变量的角度去理解threadlocal,那么它的用法,其实跟变量就一样了。定义和使用变量,应该大家都会吧。
int a;
a = 10;
上面就是变量的用法,threadlocal的用法也是类似的:
//声明一个threadlocal变量
ThreadLocal<Integer> b = new ThreadLocal<Integer>();
//赋值
b.set(10)
//取值
b.get()
上面的a和b就是两个变量。大家都会用了吧。
一定要记住:把线程这个虚幻的东西从脑袋里面抛弃,只留下变量这个东西,大家是不是觉得threadlocal一下子变得更亲切了。
我再次强调一下:threadlocal的理解,重心是把它当做变量,一定要记住:把线程这个虚幻的东西从脑袋里面抛弃,只留下变量这个东西。其他普通的变量怎么用,你就怎么玩threadlocal。
你要是还存在线程的思想,自己就把自己困住了。你不敢动它,你不敢用它,你害怕它,你害怕它给你出bug。因为你害怕线程。但是,变量这个东西,你肯定不害怕。
现在已经解决了害怕的问题,那么我们应该再往前走一步,思考一下:
既然有了那么多类型的变量,例如全局变量,局部变量,那为什么还要有threadlocal呢?
在思考这个问题的时候,我会把线程再捡回来。大家要明白我的思路:我首先是把线程扔掉,只保留变量这个东西,然后我从变量入手,最后把线程捡回来。我的思路是这样的:变量->变量的分类->线程变量。其他人的思路是:线程->线程冲突->线程变量。
上面的两条思路,方向不一样,思考的高度不一样,境界不一样的。大家要仔细体会。
我认为,ThreadLocal类是修饰变量的,重点是在控制变量的作用域,初衷可不是为了解决线程并发和线程冲突的,而是为了让变量的种类变的更多更丰富,方便人们使用罢了。很多开发语言在语言级别都提供这种作用域的变量类型。
根据变量的作用域,可以将变量分为全局变量,局部变量。简单的说,类里面定义的变量是全局变量,函数里面定义的变量是局部变量。
还有一种作用域是线程作用域,线程一般是跨越几个函数的。为了在几个函数之间共用一个变量,所以才出现:线程变量,这种变量在Java中就是ThreadLocal变量。
全局变量,范围很大;局部变量,范围很小。无论是大还是小,其实都是定死的。而线程变量,调用几个函数,则决定了它的作用域有多大。
ThreadLocal是跨函数的,虽然全局变量也是跨函数的,但是跨所有的函数,而且不是动态的。
ThreadLocal是跨函数的,但是跨哪些函数呢,由线程来定,更灵活。
class TreadLocalDemo
{
int m = 0; //全局变量
ThreadLocal<Integer> iThreadLocal = new ThreadLocal<Integer>();//线程变量
void main()
{
int n = 0;//局部变量
}
void entry1()
{
int temp = iThreadLocal.get();
}
void entry2()
{
int temp = iThreadLocal.get();
}
void entry3()
{
int temp = iThreadLocal.get();
}
}
假设有三个线程,则对应三种线程变量的三个不同的作用域:
thread1: entry1-> entry2
thread2: entry2-> entry3
thread3: entry1-> entry2-> entry3
如上,线程变量的作用域更灵活吧。一个线程一个变量,而且线程跨越多少个函数,则这个变量也跨越多少个函数。
总之,ThreadLocal类是修饰变量的,是在控制它的作用域,是为了增加变量的种类而已,这才是ThreadLocal类诞生的初衷,它的初衷可不是解决线程冲突的。
从变量的角度入手,根据变量的作用域,牵涉出:局部变量,全局变量,还有线程变量。这样理解,是不是更接地气了。地气就是变量,这是编程的最底层最根本的知识。
把threadlocal挂在变量这条线上,你有底气,你有信心,因为你熟悉变量;但是把threadlocal挂到线程这条线上,你会犯晕,线程是啥,我们不知道,因为看不见摸不着。
第三部分:我说一下threadlocal的内存泄漏问题。
对于普通的变量而言:
int a;
a = 10;
如上代码所示,10存储到了变量a里面。对比一下:
//声明一个threadlocal变量
ThreadLocal<Integer> b = new ThreadLocal<Integer>();
//赋值
b.set(10)
在上面这个代码里面,10是放在了b里面吗?【这一点令人非常的困惑!】一定要记住,10不是放在了b里面。10和b是两个独立存放的东西,不是包含关系。
Java程序跑起来的时候,它是运行在一个容器里面,这个容器就是jvm,10存放在这个容器里面,b也是存放在容器里面,b只是帮忙把10存放到了容器里面,而不是把10放到它的里面。
10和b是两个独立存放的变量,如果其中的一个被清理,那么另外一个不受影响的。
举个例子来说,两名地下党A和B,A是上线,B是下线,但是他们的生死都是独立,但是B不能直接联系党中央的,他需要通过A来帮忙传话。一旦A发生意外,B就找不到了,党中央就找不到B了,这个就是内存泄漏的原因。
回到代码里面,继续分析一下。10和b两个的独立存放的东西,只不过我们不能直接访问到10,必须通过b来传话。b存放到容器的时候,它又被包装成了弱引用,也就是说它被打了一个标签,这样它很容易被gc。一旦b被清理了,10就找不到了,从而造成了内存泄漏。
b给10的传话过程的怎么实现的,它们是通过key-value这种关系建立了传话机制:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Thread t = Thread.currentThread();
上面这个t,大家可以想象成jvm容器,因为我们可以在它里面存放东西。
t.threadLocals就是t里面的存放点,这是一个只有主卧和次卧的房间。
主卧为key,次卧为value,b住主卧,10住次卧,两者是独立的存储,不是包含的关系。
第四部分:我最后总结一下。
(1)我从c/c++中加深了对变量这个东西的认识,然后我在Java中抛弃了线程,只从变量的角度分析threadlocal。
(2)对比了一下普通变量的用法,大家可以举一反三,跟使用普通变量一样,使用threadlocal。
(3)一定要记住,10不是放在了b里面。10和b是两个独立存放的东西,不是包含关系。两名地下党A和B,A是上线,B是下线,但是他们的生死都是独立,但是B不能直接联系党中央的,他需要通过A来帮忙传话。一旦A发生意外,B就找不到了,党中央就找不到B了,这个就是内存泄漏的原因。
如果代码看不懂,没事的,你要明白10和b是相互独立的,知道这个地下党的故事就明白内存泄漏的原因了。
答疑:
提问:那为什么要设计10和b是独立的呢,怎么才内存不泄露?
回答:如果设计的时候,不是独立的,b和10是一起的,结果要么是全局变量,要么是局部变量。但是如果独立开的话,就能变成线程变量。10就是一个线程变量,不同的线程其值不一样。有的是10,有的可能改变了变成9或者8等。
理解了内存泄漏的原因,那就好说了。清空map,不就完事了。上线已经死了,下线已经发挥不了作用了。赶紧清空吧。或者,也可以把key为null的都清理掉。jdk有自己的实现,但是你们要是明白了内存泄漏的原因,你可以想出自己的解决方案的。没有必要局限于jdk