问题现象
从数据库表中发现业务中一些用户信息丢失, 对这些问题数据进行分析, 数据产生有2中方式
- 普通业务,即单线程处理, 这类数据都是完整的
- 批量业务,通过Spring线程池创建多个线程,同时处理多个任务, 发现这类数据中的用户信息都丢失了
查询代码发现程序中用户信息是保存到主线程的线程变量ThreadLocal中, 因此怀疑和ThreadLocal 有关系
private static final ThreadLocal<ApplicationUserHolder> LOCAL_USER = new ThreadLocal<ApplicationUserHolder>();
问题原因
Threadlocal而是一个线程内部的存储类,提供了线程内存储变量的能力,它可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据, 有点儿类似每个线程都有个独立的盒子,将自己线程信息放到盒子中,不同线程通过这个盒子得到自己的数据
//查看ThreadLocal#set方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
//1.得到当前的线程
Thread t = Thread.currentThread();
//2.通过 getMap(t)创建ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3.将数据存储到ThreadLocalMap
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
//再看ThreadLocalMap map = getMap(t)的实现
void createMap(Thread t, T firstValue) {
//4.在线程中创建一个ThreadLocalMap
//这样每个线程都持有一个ThreadLocalMap 对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//通过set保存数据后通过get获取
public T get() {
//5. 得到当前线程
Thread t = Thread.currentThread();
//6. 从线程中得到ThreadLocalMap
ThreadLocalMap map = getMap(t);
//7. 再从map中取值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
从上面的一段代码看,如果有3个线程,那么每个线程保存的数据是保存到线程中的ThreadLocalMap 中, 这样3个线程的持有自己的数据信息。
思考一下可以发现子线程的ThreadLocal信息是不能继承父线程的数据。 回到遇到的问题本身, 用户数据保存到主线程的线程变量里,子线程是线程池管理并维护的,所以子线程中Thread.currentThread得到持有的ThreadLocalMap 是空集合。 因此子线程中使用get方法是获取不到任何数据
解决方法
- 通过ThreadLocal 的子类 InheritableThreadLocal 类解决上述问题
/**
* This class extends {@code ThreadLocal} to provide inheritance of values
* from parent thread to child thread: when a child thread is created, the
* child receives initial values for all inheritable thread-local variables
* //注意这句话:大意是创建子线程的时候,子线程接收父线程的全部本地变量
*
* 略
*
* @author Josh Bloch and Doug Lea
* @see ThreadLocal
* @since 1.2
*/
public class InheritableThreadLocal<T> extends ThreadLocal<T>{
protected T childValue(T parentValue) {
return parentValue;
}
void createMap(Thread t, T firstValue) {
//1.创建map的时候是inheritableThreadLocals,儿不是t.threadLocals
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap getMap(Thread t) {
//2.获取的时候从inheritableThreadLocals取,
return t.inheritableThreadLocals;
}
}
看到这里的时候感觉一头雾水,为什么改变inheritableThreadLocals 后就不一样了,查了下Thread 中使用inheritableThreadLocals 的地方才明白
//1.创建线程的时候都会调用这个私有方法
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//略...
//这里有个判断,如果父线程持有的inheritableThreadLocals 不为空时,就将
//父线程中的inheritableThreadLocals设置到子线程中
//原来如此!
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//略...
}
在代码中可以看到,当使用InheritableThreadLocal 时候
- 主线程中中保存了数据后, 数据保存到Thread中的inheritableThreadLocals, 当创建子线程的时,会将备份一份父线程中的inheritableThreadLocals中的数据到子线程中,这样子线程使用get方法就得到了父线程线程变量中的数据
总结
1> ThreadLocal是解决线程安全问题,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的问题, 但是这里有一点需要注意避免脏读问题
- 现在常用的服务器比如tocat, jboss等都是通过线程池来处理请求,所以请求可能使用同1个线程池里的线程, 比如用户A用线程1, 过一段时间用户B也使用线程1访问
- 使用线程变量时,数据通过key-value方式保存到Thread中,如果程序处理不当可能B使用线程1的时候,从线程变量里得到A的某些数据,因此线程变量不再使用后最好finally清空
2> 当子线程需要使用主线程的线程变量的时候使用 InheritableThreadLocals对象 而非ThreadLocal对象