ThreadLocal错误使用方式

296 阅读2分钟

背景

有时候,在生产的代码里面发现在使用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的情况。