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<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();
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容