39

什么,你的 ThreadLocal 内存泄漏了?

 5 years ago
source link: https://mp.weixin.qq.com/s/mH1jRiZTiHdlMBSwu3f2zg?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

微信公众号: IT一刻钟

大型现实非严肃主义现场

一刻钟与你分享优质技术架构与见闻,做一个有剧情的程序员

关注可第一时间了解更多精彩内容,定期有福利相送哟。

又是一个风和日丽的早上。

这天小美遇到了一个难题。

FvEzQ3E.jpg!web

原来小美在做用户服务鉴权的时候,需要根据每个请求获取token:
//获取认证信息
Authentication authentication =
tokenProvider.getAuthentication(jwt);
//设置认证信息
SecurityContext.setAuthentication(authentication);

然后经过层层的调用,在业务代码里根据认证信息进行权限的判断,也就是鉴权。

小美心里琢磨着,如果每个方法参数中都传递SecurityContext信息,就显的太过冗余,而且看着也丑陋。

那么怎么才能隐式传递参数呢?

这个当然难不倒小美,她决定用ThreadLocal来传递这个变量:

class SecurityContextHolder {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<SecurityContext>();

public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
contextHolder.set(createEmptyContext());
}
return ctx;
}
}
......(省略不必要的)
SecurityContextHolder.getContext().setAuthentication(authentication);

整体思路上就是将SecurityContext放入ThreadLocal,这样当一个线程缘起生灭的时候,这个值会贯穿始终。

完美,小美喜滋滋的提交了代码,然后发布出去了。

结果第二天系统就出现异常了,明明是这个用户A的发起的请求,到了数据库中,却发现是操作人是用户B的信息,一时间权限大乱。

完蛋了。。。

7VfeIvU.jpg!web

这是为什么呢?

我们得先扯一扯ThreadLocal,Thread,ThreadLocalMap之间的爱恨情仇。

Ijai2aE.jpg!web

ThreadLocal原理图

图片解说:

1.Thread即线程,内部有一个ThreadLocal.ThreadLocalMap, key值是ThreadLocal,value值是指定的变量值

2.ThreadLocalMap内部有一个Entry数组,用来存储K-V值,之所以是数组,而不是一个Entry,是因为一个线程可能对应有多个ThreadLocal;

3.ThreadLocal对象在线程外生成,多线程共享一个ThreadLocal对象,生成时需指定数据类型, 每个ThreadLocal对象都自定义了不同的threadLocalHashCode

4.ThreadLocal.set 首先根据当前线程Thread找到对应的ThreadLocalMap,然后 将ThreadLocal的threadLocalHashCode转换为ThreadLocalMap里的Entry数组下标,并存放数据于Entry[]中

5.ThreadLocal.get 首先根据当前线程Thread找到对应的ThreadLocalMap,然后 将ThreadLocal的threadLocalHashCode转换为ThreadLocalMap里的Entry数组下标,根据下标从Entry[]中取出对应的数据;

6.由于Thread内部的ThreadLocal.ThreadLocalMap对象是每个线程私有的,所以做到了数据独立。

于是我们知道了ThreadLocal是如何实现线程私有变量的。

但是问题来了,如果线程数很多,一直往ThreadLocalMap中存值,那内存岂不是要撑死了?

当然不是,设计者使用了

弱引用

来解决这个问题:

static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

不过这里的弱引用只是针对key。每个key都弱引用指向ThreadLocal。当把ThreadLocal实例置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被GC回收。然而,value不能被回收,因为当前线程存在对value的强引用。只有当前线程结束销毁后,强引用断开,所有值才将全部被GC回收,由此可推断出,只有这个线程被回收了,ThreadLocal以及value才会真正被回收。

听起来很正常?

fY3eAvq.jpg!web

那如果我们使用线程池呢?常驻线程不会被销毁。这就完蛋了,ThreadLocal和value永远无法被GC回收,造成内存泄漏那是必然的。

而我们的请求进入到系统时,并不是一个请求生成一个线程,而是请求先进入到线程池,再由线程池调配出一个线程进行执行,执行完毕后放回线程池,这样就会存在一个线程多次被复用的情况,这就产生了这个线程此次操作中获取到了上次操作的值。

怎么办呢?

uiEVVz2.jpg!web

解决办法就是每次使用完ThreadLocal对象后,都要 调用其remove方法 ,清除ThreadLocal中的内容。

示例:

public class ThreadLocalTest {
static ThreadLocal<AtomicInteger> sequencer = ThreadLocal.withInitial(() -> new AtomicInteger(0));
static class Task implements Runnable {
@Override
public void run() {
int value = sequencer.get().getAndIncrement();
System.out.println("-------"+value);
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(
2);
executor.execute(new Task());
executor.execute(new Task());
executor.execute(new Task());
executor.execute(new Task());
executor.execute(new Task());
executor.execute(new Task());
executor.shutdown();
}
}

输出:

0

1

0

2

3

1

这里就是错误的。

如果每次执行完调用remove:

@Override
public void run()
{
int value = sequencer.get().getAndIncrement();
System.out.println("-------"+value);
sequencer.remove();
}

输出:

0

0

0

0

0

0

输出则正常。

好了,本期就说到这里, 转发加在看 ,是我分享的最大动力~

qu6nIni.jpg!web

ru guo jue de bu cuo, dian ji yi xia guang gao zhi chi wo yi xia ~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK