背景
有时候,在生产的代码里面发现在使用ThreadLocal的地方,有些小伙伴用得很酸爽,但是在用到最后的地方,并没发现对当前线程存储的数据进行删除处理,然后导致生产查询到的数据有部分错乱,最终的表现就是数据不一致,同时,ThreadLocal源码中保存数据主要是通过ThreadLocalMap进行保存的,核心的方法是put(key,vlaue),而key是为ThredLocal的弱引用,GC的时候key会被干掉,但对应的value还是存在的,如果用完threadlocal不进行remove的话,会造成内存泄漏的问题。
数据不一致的问题复现
由于我们的服务一般采用的是tomcat,而tomcat默认设置的线程数是200,配置参数为:server.tomcat.max-threads=200,当然,我们也可以跟进自己的资源进行更改配置,如果需要快速复现问题,直接把上面的参数配置为1,马上就可以看神奇的效果。
为了简单模拟,这里进行的简单的测试方式,即设置一个线程池(3个固定线程数据,没个线程数存储一个数字),见如下代码
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Integer> demoData=new ThreadLocal<>();
ExecutorService executorService = Executors.newFixedThreadPool(3);
for(int i=1 ;i<=4;i++){
int finalI = i;
executorService.execute(()->{
demoData.set(finalI);
System.out.println("存值-"+Thread.currentThread().getName()+"#"+ demoData.get());
});
}
executorService.awaitTermination(1, TimeUnit.MINUTES);
}
执行之后打印的结果如下
存值-pool-1-thread-1#1
存值-pool-1-thread-2#2
存值-pool-1-thread-1#4
存值-pool-1-thread-3#3
复现 :线程1的开始值是1,之后变成了4
核心原因是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);
}
/**
* key-the threadlocal obj
* value-the value to be 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();
}
正确使用的方式
在使用结束后的当前线程下,在finally代码块内对当前线程的值进行删除
finally{
threadlocal.remove();
}
问题泄漏问题的复现
可以使用多线程不断的执行threadlocl的添加查询操作,但是不进行remove,在运行过程中,通过Jconsole观察GC的情况,设置-Xmx,-Xms 小一些,就很容易模拟出OOM的情况。