美文网首页
ThreadLocal使用诡异现象

ThreadLocal使用诡异现象

作者: 缄默的石头 | 来源:发表于2019-01-30 18:07 被阅读79次

    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<T extends Map<Object, Object>> extends InheritableThreadLocal<Map<Object, Object>> {
            protected Map<Object, Object> initialValue() {
                return new HashMap<Object, Object>();
            }
    
            protected Map<Object, Object> childValue(Map<Object, Object> parentValue) {
                if (parentValue != null) {
                    return (Map<Object, Object>) ((HashMap<Object, Object>) parentValue).clone();
                } else {
                    return null;
                }
            }
        }
    

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

    public class InheritableThreadLocal<T> extends ThreadLocal<T> {
        
        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<Integer> 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<Object> firstFuture = executor.submit(() -> {
                System.out.println("first task:"+inheritableThreadLocal.get());
                return null;
            });
            inheritableThreadLocal.remove();
            inheritableThreadLocal.set(2);
            Future<Void> 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<Object> key = (ThreadLocal<Object>) 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<Object> firstFuture = executor.submit(() -> {
        try {
            inheritableThreadLocal.set(1);
            System.out.println("first task:"+inheritableThreadLocal.get());
            return null;
        }finally {
            inheritableThreadLocal.remove();
        }
    });
    inheritableThreadLocal.remove();
    

    相关文章

      网友评论

          本文标题:ThreadLocal使用诡异现象

          本文链接:https://www.haomeiwen.com/subject/msbcsqtx.html