为什么要学习ThreadLocal呢?因为面试官经常问,而且在线程中使用它可以给我们提供一个线程内的本地局部变量,这样就可以减少在一个线程中因为多函数之间的操作导致共享变量传值的复杂性,说白了,我们使用ThreadLocal可以做到在一个线程内随时随地的取用,而且与其他的线程互不干扰。
在一些特殊的情景中,应用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这个类给的注释,我们简单翻译一下就是:
此类提供线程局部变量。这些变量与正常变量不同,因为每个访问一个线程(通过其{@code get}或{@code set}方法)的线程都有其自己的,独立初始化的变量副本。 {@code ThreadLocal}实例通常是希望将状态与线程相关联的类中的私有 静态字段(例如 用户ID或交易ID)。
”什么意思呢?我们大致能够看明白,说是TreadLocal可以给我们提供一个线程内的局部变量,而且这个变量与一般的变量还不同,它是每个线程独有的,与其他线程互不干扰的。
现在我们简单的对ThreadLocal有了认识,下面我们就直接上代码,看看它的一个实际应用例子。
先来看一段代码:
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长啥样?
我们先来看看它的这个构造函数:
这其实也是Entry类的源码,其中有一个构造函数,传入key和value,在Entry中还定义了一个Object类型的value变量,把随构造函数传入进来的value值赋值给这个Object类型的value变量,这样就将value保存在了Entry中了。
我们再来看这个Entry的实现,它是继承了WeakReference<ThreadLocal>,这个是啥?WeakReference>是一个弱引用类型,简单说,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函数是如何实现重新扩充并重新计算位置的
如果达到了增长因子,那就需要重新扩充,而且还需要将所有的对象重新计算位置,我们来看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做了一个较为全面和深入的分析,大家应该也对它有了更深的印象,下面针对本文来做一个简单的总结:
1、ThreadLocal是用来提供线程局部变量的,在线程内可以随时随地的存取数据,而且线程之间是互不干扰的。
2、ThreadLocal实际上是在每个线程内部维护了一个ThreadLocalMap,这个ThreadLocalMap是每个线程独有的,里面存储的是Entry对象,Entry对象实际上是个ThreadLocal的实例的弱引用,同时还保存了value值,也就是说Entry存储的是键值对的形式的值,key就是ThreadLocal实例本身,value则是要存储的数据。
3、TreadLocal的核心是底层维护的ThreadLocalMap,它的底层是一个自定义的哈希表,增长因子是2/3,增长因子也可以叫做是一个阈值,底层定义为threshold,当哈希表容量大于或等于阈值的3/4的时候就开始扩容底层的哈希表数组table。
4、ThreaLocalMap中存储的核心元素是Entry,Entry是一个弱引用,所以在垃圾回收的时候,ThreadLocal如果没有外部的强引用,它会被回收掉,这样就会产生key为null的Entry了,这样也就产生了内存泄漏。
5、在ThreadLocal的get(),set()和remove()的时候都会清除ThreadLocalMap中key为null的Entry,如果我们不手动清除,就会造成内存泄漏,最佳做法是使用ThreadLocal就像使用锁一样,加锁之后要解锁,也就是用完就使用remove进行清理。
★
本文原创作者:ithuangqing 转载请注明出处,微信公众号开白请联系我微信H653836923
”
▼ 庆哥有一个梦想,写一些能让小白看得懂学得会的技术教程,帮助初学者更快的入门与进阶,于是乎,在编码之外开启了逐梦之旅!关注公众号,后台回复“庆哥”,2019最新java自学资源立马送上!
长按二维码识别关注!