对ThreadLocal的理解
1、为什么需要ThreadLocal?
我们在做web应用时,会用到一些跟线程绑定的变量,举个例子:
1、每个用户登录后拿到属于自己的令牌token,这个token标识了用户的身份,这个是要在一个线程中贯穿始终的,比如在Spring MVC中在controller,Service或者其他对象中都可能需要获取登录用户的信息,这个时候就要根据这个token去取;
2、再比如日志信息,我们在打印日志的时候往往希望日志信息要带有一个标识,让开发者能知道这些日志是由同一个线程打印的,不然在分析日志的时候就很混乱,开发人员完全不知道日志的相关性;
怎么样在一个线程中都可以取到一些全局变量呢?上述这些,都是ThreadLocal 的应用,如果没有ThreadLocal,那么A用户拿到的token,就并不是线程安全的,会被后来登录的用户冲掉;
2、ThreadLocal原理
为什么Thread是线程安全的?我们可以从源码大致分析一下,首先看Thread类:
ThreadLocal.ThreadLocalMap threadLocals = null;
Thread类中有一个ThreadLocalMap 类型属性,看下这个内部类:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
摘抄了一部分源码,我们大致分析一下, 首先这个Map中有一个Entry数组,然后看下Enrty类,这个类继承了WeakReference类(想了解弱引用的可以具体百度一下,这里不展开讲述),这里只讲出一个结论,仅被弱引用引用的对象在每次GC的时候都会(大概率会)JVM回收掉,为什么这里要设计成弱引用呢,我们暂时不回答这个问题,接着往下看,这里关注一下ThreadLocal的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);
}
从源码看出,其实ThreadLocal的set方法就是获取到当线程,然后往这个线程对象中的ThreadLocalMap中插入了一个键值对,这个key就是代表ThreadLocal对象(这里注意代表的是ThreadLocal对象,而不是你set进去的对象),而value才是我们真正的对象,举个例子:
public void setThreadLocal() {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("123");
}
上述代码大致相当于在ThreadLocalMap中插入了一个这样的数据结构:
{
new ThreadLocal<>():"123"
}
如果说这个上面的setThreadLocal()方法执行完之后,threadLocal 在方法中的引用就结束了。
如果说Entry对key的引用为强引用,根据JVM内存回收算法我们知道,只要当前线程对象没有消亡,那么这个ThreadLocal对象就始终没办法被回收掉(因为始终存在一条从Thread->ThreadLocalMap->Entry的强引用链)
所以Entry对象对key的引用才被设置成了弱引用,这个情况下,如果外界对ThreadLocal对象失去了引用,JVM就可以在下次GC的时候将这个ThreadLocal对象给清除掉,这样这些map对应的key就变成了null。但是这里还是有个问题,虽然Entry对key(ThreadLocal对象)是弱引用,但是对value却是强引用,那这么时候对应的value怎么办呢?显然,ThreadLocal的设计非常巧妙,他在set,get,和remove方法执行的时候都会把map中值为null的value给清理掉。
3、内存泄露
这么看起来,ThreadLocal已经防止的内存泄露,为什么我们还要在用完每个ThreadLocal 之后把他给remove掉呢:我们看一个例子
@GetMapping("/testThreadLocal")
public void testThreadLocal() {
ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
threadLocal.set(new byte[1024*1024*512]);
}
这个代码执行后,ThreadLocal的引用失效了,我们的堆内存却始终居高不下,哪怕我手动执行了GC
再看一个例子:
@GetMapping("/testThreadLocal2")
public void testThreadLocal2() {
new Thread(() -> {
ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
threadLocal.set(new byte[1024*1024*512]);
}).start();
}
从这里例子我们可以看出来,这个时候我手动后GC后竟然又回收了,这到底是为什么呢?
原因就是,Tomcat中处理每个请求其实底层维护的也是一个线程池,这个请求结束后,线程数如果没有超过核心线程数,那么线程是不会消亡的,如果不使用remove方法,那么这个线程中ThreadLocal存的对象就始终不会被回收,而这时Map中的key已经被置位null,基本上是永远也无法访问到这个对象的,所以就会造成内存泄露。
我们将第一个例子改成如下:
@GetMapping("/testThreadLocal")
public void testThreadLocal() {
ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
threadLocal.set(new byte[1024*1024*512]);
threadLocal.remove();
}
可以看到,手动GC后明显释放了内存,。
结论:所以,在使用ThreadLocal对象后,如果确定接下来不用了,一定要及时的remove掉。尽管你认为自己没有用到线程池,但是实际上,大多数情况下其实都是显示或者间接的用到了线程池。