ThreadLocal总结
作者:互联网
ThreadLocal
1、ThreadLocal能干嘛
ThreadLocal是能够在线程上下文中保存数据的,保证每个线程都能够访问到自己当前线程中独有的数据。
避免当前线程访问到其他线程的数据。
如上所示,安琪拉的请求数据只会由ThreadA处理,妲己的请求数据只会由ThreadB处理。
保证数据不会串行。
2、ThreadLocal怎么实现的
理解实现之前,先熟悉一下三个类:Thread、ThreadLocal、ThreadLocalMap
Thread:
线程,这个再简单不过。
ThreadLocal
设计目的就是为了让当前线程携带数据。
ThreadLocalMap
设计的目的就是为了维护当前线程的本地变量而设计的
三者之间的关系
1、Thread是携带数据的载体,因为数据需要在线程上下文中流转;
2、ThreadLocal就是对保存在Thread上下文的数据进行操作的;
3、ThreadLocalMap是保存数据的结构;
ThreadLocalMap是一个存储的数据结构;ThreadLocal是操作数据的动作;而Thread则是操作数据的载体;
ThreadLocal可以操作ThreadLocalMap在线程创建之后、销毁之前,在线程中存储、取出、销毁数据。
三者之间的结构
Thread中声明ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
而ThreadLocalMap确实ThreadLocal中的内部类
为什么要这样子来进行设计?
因为对于线程来说,在线程上下文中没有必要一定要携带数据。有时候需要携带,有时候不需要携带。
那么定义在Thread中是没有必要的,所以这也就是为什么说在Thread中只是声明而已。
那么为什么要在ThreadLocal中来进行定义?
因为ThreadLocalMap的设计意义就在于维护线程的本地变量,这里做的事情仅仅只是存储。
而ThreadLocal是来操作当前线程携带的数据的。
目的就是一个:能够让线程携带上数据,也能够让线程不携带数据。
问题
为什么要使用ThreadLocal来作为ThreadLocalMap的key,而不是Thread来作为key
可能Thread来作为key对于新手来说是更合理的。
但是这里想一个问题:对于一个线程来说,不可能只保存一条数据,可能是多个,也可能是不保存数据。
那么如果利用线程作为key,可以保存数据;那么要存储多条数据的时候如何保存呢?
线程要保存数据A,key是当前线程对象;线程要保存数据B,key是当前线程对象的话,因为底层是map结构,map结构继续put的时候是会覆盖掉原来的值的。
那么这样子将会导致整个线程只能携带一个数据,而不是多个数据。
而利用ThreadLocal来作为key,只需要在保存数据的时候创建一个新的ThreadLocal对象即可。保存多个数据,只要多个ThreadLocal对象即可。
源码篇
我觉得对于这里比较绕的东西,过多的解释不如看源码。
通过一个小案例来进行讲解:
public class ThreadLocalUtil {
private static final ThreadLocal<User> USER_THREAD_LOCAL = new ThreadLocal<>();
public static void setUserThreadLocal(User user){
USER_THREAD_LOCAL.set(user);
}
public static User getUser(){
return USER_THREAD_LOCAL.get();
}
public static void remove(){
USER_THREAD_LOCAL.remove();
}
}
我们都知道对应的使用顺序:set--->get-->remove
那么底层到底做了什么?
set方法
直接看源码:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
获取得到当前线程,然后获取得到当前线程的ThreadLocalMap。
偷偷瞄一眼获取得到ThreadLocalMap的方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
因为上面也看到了,声明的时候是为null的。所以判断如果是null的时候,那么接下来就是要来创建当前Thread线程对象的ThreadLocalMap了
看看具体的创建过程:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这里也很简单,就是直接创建了一个ThreadLocalMap。
对于线程来说,因为创建了ThreadLocalMap之后,下次再来放入数据的时候,因为这个ThreadLocalMap已经存在了,所以做的事情就是继续添加,这个很好理解。
注意点
注意在构造函数中,this代表的是当前对象,也就是threadlocal对象,也就是调用set方法的threadlocal对象。
看看这里的构造方法和上面的set方法是不是最相似?其实是一样的操作。
那么接下来看下构造中做了什么,但是这就涉及到了ThreadLocalMap的设计,所以这个放在下面讲。
get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
因为当前线程中在进行set的时候,已经创建过了ThreadLocalMap,直接获取得到即可。
获取得到ThreadLocalMap之后,那么接下来就应该获取得到我们存入到线程中的数据了。
因为放入进去的key是threadlocal对象,所以这里get的时候,也是通过threadlocal对象获取得到的,然后转换返回得到对象。
但是这里的判断操作,说明没有得到对应的map。那么也就只有一种情况了,没有经过set创建map,而是直接经过get来进行操作,获取得到对应的值。没有经过初始化创建对应的map,那么也就没有对应的value值了。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
第一行就是ThreadLocal中的初始化方法:
protected T initialValue() {
return null;
}
也就是说,一个类继承ThreadLocal类重写了initialValue方法,那么也会把值设计到ThreadLocalMap中来。
但是通常不会来这样子操作。这里只是留了一个点,让开发者自己来进行实现。
remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
这里也是十分简单的,直接将key移除掉,下次再来根据ThreadLocal来进行找的时候就找不到对应的value了。
那么要是按照这样子想的话,那也太不把设计源码的人当回事了吧。
下来来看看ThreadLocalMap的设计
ThreadLocalMap
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
可以看到在ThreadLocalMap中的key-value组成的对象是Entry,每个对象一组。而这里的K又继承了WeakReference若引用。
若引用表示的是在JVM运行的任意期间都可以销毁掉。那么这里的K为什么要来集成若引用?
那么这里通过图来看比较合适:
key是强引用
在线程运行期间,threadlocal引用的生命周期比较短,那么肯定是先消失,而堆中的引用还在。而因为线程对象还在,意味着threadlocalmap还在,那么key将会执行堆中的threadlocal对象,那么就会导致了threadlocal无法被JVM回收,将会导致内存泄漏。
而如果线程的生命周期也很短,那么将没有这个内存泄漏问题。最终都会被回收掉。
而我们经常在web开发中使用到thread,而web中内嵌了一个线程中,那么将会导致线程无法销毁,那么将会出现内存泄漏问题。
所以如果key是强引用+web开发中,那么内存泄漏是一定的。
key是弱引用
弱引用表示的是JVM随时都可以回收掉这个对象。
那么当threadlocal引用生命周期消逝了之后,map中的key弱引用到了threadlocal在堆中的地址,那么显然key是会被回收掉的。
如果thread生命周期短,那么也不会出现问题。
但是如果是在web开发模式下,thread是在线程池中的,不会消失的。那么将会导致这里的value永远无法获取得到。
而导致了value的内存泄漏。
解决办法
那么解决办法是什么?????
手动的将value给清除掉。而remove方法就是来做这个事情的。因为上面没有分析remove方法的具体实现,下面来看看:
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
看到第三行源代码的时候,应该很熟悉了。线性探测法,找到threadlocal在数组中的下标,因为可能会涉及到hash碰撞,所以这里也有考虑。
而根据key获取得到value的时候,这里的操作可以看下:
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
首先将这个entry置空,也就是说threadlocalmap中数组指定位置置为空。那么threadlocal和value还存在,因为threadlocal是弱引用,可以被回收掉,而value可是强引用。那么应该来对value做一下操作。而expungeStaleEntry方法就是来做这个事情的:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
而我们首先可以看到已经将value置为了null。其实下面的操作无非就是遍历整个数组,做检查。因为JVM回随时回收掉若应用,这里就是不断的来进行检查。
我们试想一种场景,在进行set未进来remove的阶段中,key被JVM回收了。
那么接下来我们进行get、remove时候发生的事情。
进行get方法的时候:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
因为threadlocalmap还在,所以可以得到对应的值,而k被回收了,就意味着找不到对应的threadlocal了,那么里面的value怎么办?
肯定是在threadlocal还在的时候,那么就可以通过threadlocal来获取得到这里的value的值。
所以不会存在在set、remove之间得不到对应的value的情况。
总结
1、三个类之间的关系?为什么要这样子设计?
2、为什么要利用Threadlocal作为key?
3、怎么才不会导致内存泄漏?
4、set和remove之间不会得不到对应的value;
标签:总结,ThreadLocalMap,value,ThreadLocal,线程,key,null 来源: https://www.cnblogs.com/likeguang/p/16372556.html