ThreadLocal使用诡异现象
ThreadLocal使用诡异现象
1. 前言
ThreadLocal不多说了,在线程中维护一个Thread.ThreadLocalMap对象,将ThreadLocal对象包装成一个WeakReference作为map的key,ThreadLocal持有的value作为map的value,从而实现线程私有。而本次遇到的问题就比较诡异了,现象如下
2. 现象
QA在线上验证功能的时候发现任务提交人跟登陆人不一致的现象,例如登陆人是张三,任务系统显示任务的提交人是李四,这种张冠李戴的现象也不是必现的,RD通过排查代码,发现在业务代码中使用了线程池提交的任务,在任务逻辑里面使用UserUtil.getUser()来获取当前用户,众所周知这种方式是通过ThreadLocal来持有User信息的。
这个现象很诡异,主要体现在两点:
- 线程池里面的线程为什么会获取到主线程私有的变量呢,在程序中没有看到有显式传递变量的代码
- 假设可以获取到父线程的变量,那为什么会出现登陆人紊乱的现象呢?
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了。
那么正确的使用姿势是什么呢?
- 线程中提交的任务就不要直接使用ThreadLocal了,可以作为任务的成员变量来传递
- 如果一定要使用的话,可以参考下面的代码
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();