为何要学习ThreadLocal呢?由于面试官常常问,并且在线程中使用它能够给咱们提供一个线程内的本地局部变量,这样就能够减小在一个线程中由于多函数之间的操做致使共享变量传值的复杂性,说白了,咱们使用ThreadLocal能够作到在一个线程内随时随地的取用,并且与其余的线程互不干扰。java
在一些特殊的情景中,应用ThreadLocal会带来极大的便利,不过不少人却搞不懂Threadlocal究竟是个啥?在咱们的面试中也常常会被问到Threadlocal,因此基于咱们的实际应用以及应对面试,咱们都有必要好好的学习下Threadlocal。面试
今天,咱们就来完完整整的学习下Threadlocal,争取之后不再学了,由于看完今天这篇文章,你就对Threadlocal忘不了了!数组
首先,咱们既然要学习Threadlocal,那么咱们先要知道它是个啥?咱们从名字来看,Threadlocal意思就是线程本地的意思,咱们这个属于猜测,并不权威,那么要想知道他是个啥,最好的办法就是看看官方是怎么定义它的,咱们看看ThreadLocal的源码(基于jdk1.8)中对这个类的介绍:微信
This class provides thread-local variables. These variables differ from* their normal counterparts in that each thread that accesses one (via its* {@code get} or {@code set} method) has its own, independently initialized* copy of the variable. {@code ThreadLocal} instances are typically private* static fields in classes that wish to associate state with a thread (e.g.,* a user ID or Transaction ID).数据结构
这是在jdk1.8中对ThreadLocal这个类给的注释,咱们简单翻译一下就是:ide
此类提供线程局部变量。这些变量与正常变量不一样,由于每一个访问一个线程(经过其{@code get}或{@code set}方法)的线程都有其本身的,独立初始化的变量副本。 {@code ThreadLocal}实例一般是但愿将状态与线程相关联的类中的私有静态字段(例如用户ID或交易ID)。函数
什么意思呢?咱们大体可以看明白,说是TreadLocal能够给咱们提供一个线程内的局部变量,并且这个变量与通常的变量还不一样,它是每一个线程独有的,与其余线程互不干扰的。学习
如今咱们简单的对ThreadLocal有了认识,下面咱们就直接上代码,看看它的一个实际应用例子。测试
先来看一段代码:this
public class Test { private static int a = 10; private static ThreadLocal<Integer> local; public static void main(String[] args) { Thread A = new Thread(new ThreadA()); A.start(); ThreadB B = new ThreadB(); B.start(); } static class ThreadA implements Runnable{ @Override public void run() { local = new ThreadLocal(); local.set(a+10); System.out.println(local.get()+Thread.currentThread().getName()); local.remove(); System.out.println(local.get()+Thread.currentThread().getName()); } } static class ThreadB extends Thread{ @Override public void run() { System.out.println(local.get()+Thread.currentThread().getName()); } } }
咱们以前就知道,ThreadLocal是为咱们提供一个线程局部变量的,那咱们测试的方法就是建立两个线程,使用ThreadLocal去存取值,看看两个线程之间会不会互相影响,上面的这段代码咱们来简单分析一下,首先是两个变量:
private static int a = 10; private static ThreadLocal<Integer> local;
注意看,这里就使用到了ThreadLocal了,使用方法和普通的变量几乎是同样的,咱们这个时候就能够把ThreadLocal按照一个变量来理解,咱们日常定义一个变量不就是这样:
int a = 10;
因此对于ThreadLocal也是同样,咱们建立一个ThreadLocal就如同新建立一个变量同样:
private static ThreadLocal<Integer> local;
这个时候咱们就定义了一个ThreadLocal,注意这个时候只是定义而没有进行初始化赋值,并不像int a = 10那样已经赋值为10了,如今的ThreadLocal还只是定义好而已,咱们继续看下面的代码:
static class ThreadA implements Runnable{ @Override public void run() { local = new ThreadLocal(); local.set(a+10); System.out.println(local.get()+Thread.currentThread().getName()); local.remove(); System.out.println(local.get()+Thread.currentThread().getName()); } } static class ThreadB extends Thread{ @Override public void run() { System.out.println(local.get()+Thread.currentThread().getName()); } }
这里是定义了两个线程,注意看了,在第一个线程中的run方法内,咱们对ThreadLocal进行了实例化:
local = new ThreadLocal();
到这里,咱们就完整的建立了一个ThreadLocal,也就是下面这样:
ThreadLocal local = new ThreadLocal();
咱们以前说能够把ThreadLocal看作是一个变量,像普通的变量,好比下面这样:
int a = 10;
就这样,咱们就给a赋值为10了,那么对于ThreadLocal而言,咱们该怎么给它设置值呢?有以下的操做:
local.set();
就像咱们上面代码那样:
local.set(a+10);
这样咱们就给ThreadLocal给赋值了,那么怎么拿到这个值呢?如同上面代码所示:
System.out.println(local.get()+Thread.currentThread().getName());
也就是经过:
local.get()
至此,咱们就知道ThreadLocal最基本的使用了。
也就是:
ThreadLocal local = new ThreadLocal(); local.set(a+10); local.get()
到这里咱们有没有以为它像是一个map,也是key-value的形式来存取值的呢?
另外在上面的代码中还有以下的一句代码:
local.remove();
这个也好理解,是删除,删除啥呢?咱们先留个疑问,接下来的文章会慢慢说,看到最后,你就明白了。
而后咱们所展现的代码还有这么一段:
Thread A = new Thread(new ThreadA()); A.start(); ThreadB B = new ThreadB(); B.start();
这个就是开启两个线程。
至此,咱们所展现的代码就简单的分析了一下,重点看了ThreadLocal是个简单的使用。
那么这段代码会输出什么结果呢?在看输出以前,咱们须要强调一点,ThreadLocal能够提供线程内的局部变量,各个线程之间互不干扰。那咱们在思考上面所展现的代码。首先是定义ThreadLocal:
接下来在第一个线程中实例化而且赋值:
而后咱们看在第二个线程中:
大眼一看,貌似以为应该仍是20,毕竟是同一个local啊,并且local在以前已经赋值了等于20,这里只不过在另一个线程中再次去取这个值,咱们来看看输出结果:
看到结果咱们知道了,虽然在第一个线程中ThreadLocal被实例化且赋值了,并且正确取值20,可是在另外一个线程中去取值的话为空,咱们再来稍微改变下代码:
哦,彷佛明白了,对于ThreadLocal而言,每一个线程都是有一个单独存在的,至关于一个副本,线程之间互不影响,这里面还有一个null是由于调用了:
local.remove();
这至关于把值删除了,天然为空,想想,上述的结果不就说明了ThreadLocal的做用吗?提供线程局部变量,每一个线程都有本身的一份,线程之间没有影响。
可能有的人不明白了,这里的local不都是这个吗?
难道不是同一个?按理说是一个啊,在另一个线程中应该取值是同样的啊,怎么会是空呢?并且在另一个线程中也只是调用了这个简单的get方法啊:
local.get()
哦,我知道了,这个可能就是get的问题,在不一样的线程之间get的实现是不一样的,那它的底层是怎么实现的呢?
好了,确定有人火烧眉毛的想看看这个get是怎么实现的,为何会出现上述的结果,那咱们就一块儿来看看这个get的底层源码:
这个就是get方法的实现了,可能咱们猛一看并不能彻底看明白每一个细节,可是大体意思已经很清楚了,接下来咱们来简单的分析一下,对了咱们如今要解决的问题是为何在另外一个线程中调用get方法以后获得的值是null,也就是这个:
咱们首先来看这两句代码:
Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t);
首先是获取当前线程,而后根据当前线程获得一个ThreadLocalMap,这个ThreadLocalMap是个啥,咱们暂时还不知道,解下来就进行了以下判断:
if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } }
也就是在判断根据当前线程获得的ThreadLocalMap是否为空,咱们想一想,咱们就是直接调用get就来到了这里,好像并灭有什么地方去建立了这个ThreadLocalMap吧,那么这里判断的就是空了,因此就会去走以下的语句:
return setInitialValue();
虽然这里咱们并无这个Map,可是咱们看若是有map的话是怎么执行呢?咱们仔细看看这段代码:
ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; }
这不就是在返回咱们须要的值嘛?这个值是从这个ThreadLocalMap中拿到的,哦,到了这里彷佛知道了,为啥在另外一个线程中调用get会获得null,那是由于值被放到了一个叫作ThreadLocalMap的东西里面了,而它又是根据当前线程建立的,也就是说每一个线程的ThreadLocalMap是不一样的,在当前线程中并无建立,因此也就没值。
嗯嗯,这个想法貌似很对,不过又有个问题,为啥会是null呢?咱们就要看这个语句的执行了:
return setInitialValue();
从这个方法的名字能够猜测,这应该是初始化操做的。咱们看看这方法是如何实现的:
在这个方法之中,首先会执行以下语句:
T value = initialValue();
咱们看看这个方法的实现:
原来就返回一个null啊,那么上面的value就是null了,而后咱们再看下面的语句,是否是以为很熟悉:
咱们知道,这里map是没有的,因此会走else,也就是回去执行以下的方法:
createMap(t, value);
对了,这里的value是个null,而t就是当前线程啦,咱们继续看看这个方法的实现:
哦,看到这里彷佛就知道,在这个方法中就建立了一个ThreadLocalMap,咱们以前看源码以为数据是被放到了这个ThreadLocalMap中了,那么如今这里已经建立了一个ThreadLocalMap,那么数据是哪一个呢?看方法实现,应该就是那个firstValue了,咱们知道这个值就是以前传过来的value,也就是null,这至关于一个value值,那么这里的key呢?是否是就是这个this,那么这个this指的谁呢?
这里的this实际上是ThreadLocal的实例,也就是以前的local:
因此到了如今,这个get方法的咱们分析的结果就是建立了一个ThreadLocalMap,而后往里面放了值,是一个key-value的形式,key就是咱们的ThreadLocal实例。
而后咱们再看执行完createMap以后的操做,就是直接返回value了,也就是一个null,因此如今咱们明白了为何这里调用get是个null。
看到这里,这个get是明白怎么回事了,那么在第一个线程中的get也是这样的吗?
对于get的方法实现确定是同样的,之因此这里获得值20,那是由于在当前线程执行了set方法:
local.set(a+10);
根据咱们以前对get的分析,这里咱们应该能够猜测到,set方法也建立了一个ThreadLocalMap而且把值放了进去,因此执行get能获得值,咱们一块儿来看下set的实现:
是否是很熟悉,也是先拿到当前线程,而后根据当前线程获得ThreadLocalMap,这里一样以前没有,因此须要从新建立,也就是去执行:
createMap(t, value);
可是这里的value就不是null了,而是传过来的20,咱们接着看这个方法的实现:
熟悉不,又到了这里,建立了一个新的ThreadLocalMap来存放数据,this一样也是ThreadLocal的实例,也就是local,这样一来,key就对应咱们的ThreadLocal实例,value就是传过来的20了,另外咱们大概知道,这么个键值对是放在ThreadLocalMap中的,而后咱们经过当前线程能够获得这个ThreadLocalMap,再根据ThreadLocal这个实例就能够获得value的值,也就是20.
咱们接下来看这个线程中的get的执行:
由于咱们在set的时候就建立了ThreadLocalMap,因此这里就不会再去建立了,由于已经有map了,因此会直接执行:
这里其实就牵涉到ThreadLocalMap的内部实现了,看到这里咱们须要来借助一张图以便加深理解,就是下面的这张图:
通过咱们上面的分析,咱们知道ThreadLocal的设置值的方式是key-value的形式,也知道了这里的key其实就是ThreadLocal的实例,value就是要设置的值。
这里咱们看下ThreadLocalMap,它实际上是一个数据结构,就是用来存放咱们的值的,并且它也是ThreadLocal的一个核心,咱们经过上面这张图,首先要知道的一点就是:
ThreadLocalMap中存储的是Entry对象,Entry对象中存放的是key和value。
至于为何是这样的,咱们一步步的来分析ThreadLocalMap!
在ThreadLocalMap中实际上是维护了一张哈希表,这个表里面就是Entry对象,而每个Entry对象简单来讲就是存放了咱们的key和value值。
那么这个是如何实现的呢?首先咱们来想,Entry对象是存放在ThreadLocalMap中,那么对于TreadLocalMap而言就须要一个什么来存放这个Entry对象,咱们能够想成一个容器,也就是说ThreadLocalMap须要有一个容器来存放Entry对象,咱们来看ThreadLocalMap的源码实现:
在ThreadLocalMap中定义了一个Entry数组table,这个就是存放Entry的一个容器
,在这里咱们首先须要知道一个概念,那就是什么是哈希表?
百度百科是这样解释的:
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它经过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫作散列函数,存放记录的数组叫作散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能获得包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
上面也提到过,ThreadLocalMap其实就是维护了一张哈希表,也便是一个数组,这个表里面存储的就是咱们的Entry对象,其实就是它:
涉及到哈希表,必然会涉及到另一个概念,那就是增加因子,那什么是增加因子呢?
简单来讲,这是一个值,当表里面存储的对象达到了表的总容量的某个百分比的时候,这张表就该扩容了,那么这个百分比就是增加因子,咱们看ThreadLocalMap中的增加因子:
从这些代码咱们能够了解到,ThreadLocalMap中定义了一个threshold属性,这个属性上面有个介绍,也就是:
The next size value at which to resize.
翻译过来就是:要调整大小的下一个大小值。
什么意思呢?也就是说当哈希表中存储的对象的数量超过了这个值的时候,哈希表就须要扩容,那么这个值具体是多大呢?下面有个方法:
它也有个注释:
Set the resize threshold to maintain at worst a 2/3 load factor.
翻译过来就是:设置调整大小阈值以保持最坏的2/3负载系数。
意思就是设定这个增加因子为总容量的2/3,这个增加因子就是threshold。也就是当哈希表的容量达到了总容量的2/3的时候就须要对哈希表进行扩容了。
到这里咱们就知道了,ThreadLocalMap维护了一个哈希表,表里面存储的就是Entry对象,当哈希表的当前容量达到了总容量的2/3的时候就须要对哈希表进行扩容了。
那么可能有人会问了,初始容量是多少啊?这个在源码中也有展示:
也便是16,那么对于数据而言,它又是怎样被放到哈希表中的呢?接下来咱们就来看看ThreadLocalMap中设置值的方法:
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(); }
咱们来一步步的分析这段源码,看看数据是如何被存储的,为了让你们更加的明白,咱们仍是从最开始的ThreadLocal设置值得时候开始一步步的进入到这段源代码,首先就是这段代码:
这是在第一个线程中,咱们对ThreadLocal进行了实例化,而且在第一个线程总开始设置值,也就是调用set方法,咱们进入到这个set方法看看:
咱们以前就分析过了,这里没有map,会去建立,咱们进入到createMap中看看:
这里建立了ThredLocalMap,调用了它的构造方法,咱们进入看看:
这段代码就须要好好解读了,首先是它:
table = new Entry[INITIAL_CAPACITY];
这个table没有忘记是啥吧,就是以前定义的Entry数组,就是这个:
这里的INITIAL_CAPACITY就是初始化容量16,因此这里就构建了一个容量为16的Entry数组,这个数组就能够用来存放咱们的数据,具体怎么存放,咱们接着往下看:
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
这里是为了获得一个下表,由于哈希表是依靠一个索引去存取值得,因此会根据这个下标值去决定把数据存放到哪一个位置,简单点就是把数据放到数组中的哪一个位置,这个就是数组下标,那这个threadLocalHashCode是个啥呢?咱们看看:
它是经过这个nextHashCode方法获得的,这个nextHashCode也有一系列的操做,反正最终目的就是为了获得一个索引值,或者是下标值,来决定这个数据存放到哪一个位置。
那为何这样写呢?
firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
这是拿获得的threadLocalHashCode对Entry数组的总容量减去一的值作取余操做,目的就是为了获得的下标值始终都在数组内,防止下标越界的。
再接着看剩下的代码:
table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY);
拿到下标值以后就获得了一个位置就是table[i],而后就是把一个具体的Entry对象放进去了,剩下的就是设置当前表中有几条数据,也就是有几个Entry对象了,而后根据初始容量设置增加因子,咱们重点来看看这段代码:
table[i] = new Entry(firstKey, firstValue);
table[i]也就是Entry数组中的一个确切的位置,是要放入一个Entry对象的,这里就new了一个新的Entry对象,并把key和value传入了进去,咱们看看这个Entry的构造方法以及这个Entry类的实现。
咱们先来看看它的这个构造函数:
这其实也是Entry类的源码,其中有一个构造函数,传入key和value,在Entry中还定义了一个Object类型的value变量,把随构造函数传入进来的value值赋值给这个Object类型的value变量,这样就将value保存在了Entry中了。
咱们再来看这个Entry的实现,它是继承了WeakReference<ThreadLocal<?>>,这个是啥?WeakReference<ThreadLocal<?>>是一个弱引用类型,简单说,Entry本质上就是一个弱引用,由于是继承WeakReference<ThreadLocal<?>>这个弱引用,因此它其实也是个弱引用,而Entry的实例说白了就是对ThreadLocal实例的一个弱引用,只不过Entry的设计上同时还保存了value值。
到这里,咱们就知道了这个Entry是如何保存键值对的了,也知道Entry其实就是个弱引用。
对了,你要知道上述咱们的分析是针对ThreadLocal第一次调用set方法的时候由于没有map须要建立map走得上述方法,若是是再次调用则会走map中的set方法,咱们具体看源码:
因为咱们在第一次调用set方法时已经建立了map,那么再次set的时候就会主席那个map的set方法,咱们来看看map的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中经过set方式设置值的源码实现,第一次调用是经过构造函数的形式设置数据,咱们如今来看看这个set方式的数据设置。
Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1);
首先是拿到以前建立的Entry数组,这里是tab,而后也是计算出一个新的下标值来存放新数据,接下来就是这段代码:
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; } }
首先要知道这是一个for循环,根据一个下标值获得一个新的Entry对象,而后进入循环条件 也便是这个Entry对象不为null,而后执行循环体,循环体中有两个判断,还有一个根据当前Entry对象获得ThreadLocal的引用,也便是Key,不过这里定义为k。
如今咱们要知道,咱们是要往Entry数组中放入一个新的Entry对象,具体放到哪里由i这个下标值肯定,具体的位置就是table[i],因此会出现的状况就有这个位置本来就有一个Entry对象或者为null,因而若是本来就有的话并且引用的是同一个ThreadLocal的话,那么就把值给覆盖掉:
if (k == key) { e.value = value; return; }
若是是这个位置是null的话,咱们就放入新的值:
if (k == null) { replaceStaleEntry(key, value, i); return; }
固然,也会出现的状况就是这个位置不为null,并且也不是同一个ThreadLocal的引用,那么就须要继续日后挪一个位置来放入新的数据:
e = tab[i = nextIndex(i, len)])
固然,这个新的位置上依然要进入判断,也是上面的状况,以此类推,直到找到一个位置要么为null,要么是同一个ThreadLocal的引用,只有这样才能放入新的数据。
咱们接着来看下面的代码,执行完上面的判断以后会执行以下的代码:
tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();
这个就是建立具体的Entry对象,由于Entry数组多了一个Entry对象,因此总条目须要加一,而这个if判断则是为了看看当前存储的对象个数是否达到了增加因子,也就是判断下是否须要扩容,若是须要扩容了该怎么办呢?这个时候要依靠的就是这个rehash函数了。
若是达到了增加因子,那就须要从新扩充,并且还须要将全部的对象从新计算位置,咱们来看rehash函数的实现:
咱们看到在if判断中判断的指标是增加因子的3/4,这是怎么回事,以前不是说增加因子是2/3嘛?超过这个值才须要扩容,这怎么变成了增加因子的3/4才开始扩容呢?咱们以前说过,ThreadLocalMap中存储的是Entry对象,Entry本质上是个ThreadLocal的弱引用,因此它随时都有可能被回收掉,这样就会出现key值为null的Entry对象,这些都是用不到的,须要删除掉来腾出空间,这样一来,实际上存储的对象个数就减小了,因此后面的判断就是增加因子的3/4,而不是增加因子2/3了。
而expungeStaleEntries();就是作这样的清理工做的,把占坑的Entry通通删除掉。
那该如何获取到Entry对象中的数据呢?也便是咱们使用ThreadLocal的实例去调用get方法取值:
由于已经有map了,因此咱们直接就调用map的getEntry方法,咱们看看这个方法的实现:
这段代码仍是比较简单的,首先根据哈希码值算出下标i,而后就肯定了这个Entry的位置,若是这个位置不为空并且对用的ThreadLocal的弱引用也是咱们须要的这个,那么就返回这个Entry对象中保存的value值。
固然,若是对应的位置为空的话,咱们就须要getEntryAfterMiss函数来进行进一步的判断了。
到了这里相信你们对ThreadLocalMap就有了必定的认识了,接下来咱们继续来聊聊ThreadLocal的内存泄露问题。
咱们在讲ThreadLocal的内存泄漏以前,首先要搞清楚什么是内存泄漏,那要提及内存泄漏,确定还有个概念须要说,那就是内存溢出,这二者是个啥呢?
首先什么是内存泄漏
:
说的简单点那就是由于操做不当或者一些错误致使没有能释放掉已经再也不使用的内存,这就是内存泄漏,也就是说,有些内存已经不会再使用了,可是却没有给它释放掉,这就一直占用着内存空间,从而致使了内存泄漏。
那什么是内存溢出呢?
这个简单点说就是内存不够用了,我运行一个程序好比说须要50M的内存,可是如今内存就剩下20M了,那程序运行就会发生内存溢出,也就是告诉你内存不够用,这时候程序就没法运行了。
好,了解了基本概念以后,咱们再来看看T和read Local的内存泄漏,那为何T和read Local会产生内存泄漏呢?咱们再来看看这张图:
通过咱们上述的讨论,咱们大体知道了ThreadLocal其实本质上是在每一个线程中单独维护了一个ThreadLocalMap数据结构,这个ThreadLocalMap
是每一个线程独有的,只有根据当前线程才能找到当前线程的这个ThreadLocalMap,这就实现了线程以前的隔离。
咱们看上面那张图,每一个线程根据找到本身维护的ThreadLocalMap,而后能够操做这个数据结构,往里面存取数据,而ThreadLocalMap中维护的就是一个Entry数组,每一个Entry对象就是咱们存放的数据,它是个key-value的形式,key就是ThreadLocal实例的弱引用,value就是咱们要存放的数据,也就是一个ThreadLocal的实例会对用一个数据,造成一个键值对。
若是有两个线程,持有同一个ThreaLocal的实例,这样的状况也就是Entry对象持有的ThreadLocal的弱引用是同样的,可是两个线程的ThreadLocalMap是不一样的,记住一点,那就是ThreadLocalMap是每一个线程单独维护的。
那咱们如今来看,为何ThreadLocal会出现内存泄漏,咱们以前也说过了,Entry对象持有的是键就是ThreadLocal实例的弱引用,弱引用有个什么特色呢?那就是在垃圾回收的时候会被回收掉,能够根据上图想一下,图中虚线就表明弱引用,若是这个ThreadLocal实例被回收掉,这个弱引用的连接也就断开了,就像这样:
那么这样在Entry对象中的key就变成了null,因此这个Entry对象就没有被引用,由于key变成看null,就取不到这个value值了,再加上若是这个当前线程迟迟没有结束,ThreadLocalMap的生命周期就跟线程同样,这样就会存在一个强引用链,因此这个时候,key为null的这个Entry就形成了内存泄漏。
由于它没有用了,可是尚未被释放。
明白了如何产生的内存泄漏,也就知道了怎么解决,通过上面的分析,咱们大体知道了在ThreadLocalMap中存在key为null的Entry对象,从而致使内存泄漏,那么只要把这些Entry都给删除掉,也就解决了内存泄漏。
咱们每次使用ThreadLocal就会随线程产生一个ThreadLocalMap,里面维护Entry对象,咱们对Entry进行存取值,那么若是咱们每次使用完ThreadLocal以后就把对应的Entry给删除掉,这样不就解决了内粗泄漏嘛,那怎么作呢?
在ThreadLocal中提供了一个remove方法:
这个就是根据key删除掉对应的Entry,如此一来,咱们就解决了内存泄漏问题,由于可能出现内存泄漏的Entry,在咱们使用完以后就立马删除了。
因此对于ThreadLocal而言,就应该像使用锁同样,加锁以后要记得解锁,也就是调用它的remove方法,用完就清理。
至此,咱们已经对ThreadLocal作了一个较为全面和深刻的分析,你们应该也对它有了更深的印象,下面针对本文来作一个简单的总结:
一、ThreadLocal是用来提供线程局部变量的,在线程内能够随时随地的存取数据,并且线程之间是互不干扰的。
二、ThreadLocal其实是在每一个线程内部维护了一个ThreadLocalMap,这个ThreadLocalMap是每一个线程独有的,里面存储的是Entry对象,Entry对象其实是个ThreadLocal的实例的弱引用,同时还保存了value值,也就是说Entry存储的是键值对的形式的值,key就是ThreadLocal实例自己,value则是要存储的数据。
三、TreadLocal的核心是底层维护的ThreadLocalMap,它的底层是一个自定义的哈希表,增加因子是2/3,增加因子也能够叫作是一个阈值,底层定义为threshold,当哈希表容量大于或等于阈值的3/4的时候就开始扩容底层的哈希表数组table。
四、ThreaLocalMap中存储的核心元素是Entry,Entry是一个弱引用,因此在垃圾回收的时候,ThreadLocal若是没有外部的强引用,它会被回收掉,这样就会产生key为null的Entry了,这样也就产生了内存泄漏。
五、在ThreadLocal的get(),set()和remove()的时候都会清除ThreadLocalMap中key为null的Entry,若是咱们不手动清除,就会形成内存泄漏,最佳作法是使用ThreadLocal就像使用锁同样,加锁以后要解锁,也就是用完就使用remove进行清理。
本文原创做者:ithuangqing
转载请注明出处,微信公众号开白请联系我微信H653836923
▼ 庆哥有一个梦想,写一些能让小白看得懂学得会的技术教程,帮助初学者更快的入门与进阶,因而乎,在编码以外开启了逐梦之旅!关注公众号,后台回复“庆哥
”,2019最新java自学资源立马送上!
长按二维码识别关注!