ThreadLocal使用诡异现象

ThreadLocal使用诡异现象

1. 前言

ThreadLocal不多说了,在线程中维护一个Thread.ThreadLocalMap对象,将ThreadLocal对象包装成一个WeakReference作为map的key,ThreadLocal持有的value作为map的value,从而实现线程私有。而本次遇到的问题就比较诡异了,现象如下

2. 现象

QA在线上验证功能的时候发现任务提交人跟登陆人不一致的现象,例如登陆人是张三,任务系统显示任务的提交人是李四,这种张冠李戴的现象也不是必现的,RD通过排查代码,发现在业务代码中使用了线程池提交的任务,在任务逻辑里面使用UserUtil.getUser()来获取当前用户,众所周知这种方式是通过ThreadLocal来持有User信息的。
这个现象很诡异,主要体现在两点:

  1. 线程池里面的线程为什么会获取到主线程私有的变量呢,在程序中没有看到有显式传递变量的代码
  2. 假设可以获取到父线程的变量,那为什么会出现登陆人紊乱的现象呢?

3. 分析

先解释第二个问题,这个比较好理解,任务逻辑执行结束后没有调用ThreadLocal的remove方法,没有清除线程池中工作线程的私有变量,导致后续任务的执行复用之前的变量。
第二个问题,猜测主线程的私有变量隐式地传递到工作线程中了,深入阅读下UserUtil.getUser()逻辑,发现使用的是一个InheritableThreadLocalMap类型的ThreadLocalMap,这个类继承了InheritableThreadLocal。

private static final class InheritableThreadLocalMap> extends InheritableThreadLocal> {
        protected Map initialValue() {
            return new HashMap();
        }

        protected Map childValue(Map parentValue) {
            if (parentValue != null) {
                return (Map) ((HashMap) parentValue).clone();
            } else {
                return null;
            }
        }
    }

InheritableThreadLocal这个类从名称上可以猜测到是可继承的ThreadLocal,这个类的源码也很简单,说实话没有看出来是怎么实现继承关系的。猜测是在父线程创建子线程时实现这个复制关系的。

public class InheritableThreadLocal extends ThreadLocal {
    
    protected T childValue(T parentValue) {
        return parentValue;
    }

    
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

4. 验证

做一个简单的实验来复现线上的问题,创建一个只有一个线程的线程池,连续两次提交任务,两次提交之间更换了InheritableThreadLocal的值来模拟用户切换,在任务中获取InheritableThreadLocal的值,看是打印出来否是和主线程一致,结果很明显不一致,完美的复现了线上的问题。

public class ThreadLocalCase {

    private static InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        inheritableThreadlocal();
    }

    public static void inheritableThreadlocal() throws ExecutionException, InterruptedException {
        inheritableThreadLocal.set(1);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
        Future firstFuture = executor.submit(() -> {
            System.out.println("first task:"+inheritableThreadLocal.get());
            return null;
        });
        inheritableThreadLocal.remove();
        inheritableThreadLocal.set(2);
        Future secondFuture = executor.submit(() -> {
            System.out.println("second task:"+inheritableThreadLocal.get());
            return null;
        });
        inheritableThreadLocal.remove();
        shutdown(executor);
    }

    private static void shutdown(ThreadPoolExecutor executor) {
        executor.shutdown();
        while (!executor.isTerminated()) {

        }
    }

}

===============

first task:1
second task:1

5. 剖析

查看Thread的构造函数,只有一个java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)方法,在这个方法内部有这么一行代码:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

inheritThreadLocals这个变量为true,表示继承ThreadLocal,parent是当前线程,也就是当前线程的inheritableThreadLocals不为null,就会把父线程的inheritableThreadLocals通过ThreadLocal.createInheritedMap传递进去,这个方法只是构造了一个ThreadLocalMap,具体逻辑如下:

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal key = (ThreadLocal) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

这样基本上就清楚了,如果父线程中的inheritThreadLocals不为空,那么在创建子线程的时候会把自己的inheritThreadLocals传给子线程,这样就完成了ThreadLocal的传递,解释了上面的第二个问题。这个地方解决hash冲突也是一个亮点

6. 总结

现在来复盘下线上问题,只创建一个线程的线程池,第一次提交任务的时候会创建新的线程,就会把主线程的inheritThreadLocals传给这个新线程,第二次提交任务的时候不会创建新线程,那么线程池中的线程由于没有执行remove动作,持有的还是老的value。
那么在任务执行结束的时候执行remove动作就OK了吗?
这样做会带来一个新的问题,第二次提交任务就不会创建新线程,线程池已有的线程remove之后,后续的任务就获取不到ThreadLocal的value了。
那么正确的使用姿势是什么呢?

  1. 线程中提交的任务就不要直接使用ThreadLocal了,可以作为任务的成员变量来传递
  2. 如果一定要使用的话,可以参考下面的代码
inheritableThreadLocal.set(1);
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
Future firstFuture = executor.submit(() -> {
    try {
        inheritableThreadLocal.set(1);
        System.out.println("first task:"+inheritableThreadLocal.get());
        return null;
    }finally {
        inheritableThreadLocal.remove();
    }
});
inheritableThreadLocal.remove();






你可能感兴趣的:(ThreadLocal使用诡异现象)