前言
系统中的登录上下文使用了ThreadLocal来记录每个请求里面用户的基本信息。用户的登录校验是通过拦截器解析请求中的cookie做相关校验。当校验通过后,loginContext中将存储用户基本信息供后续接口调用使用。在项目开发中,由于缺乏对ThreadLocal的深刻认识导致出现过一次惊魂的线上bug。
一、ThreadLocal基本源码分析
1、首先是创建一个ThreadLocal的holder变量,holder=new ThreadLocal();源码中就是一个简单的无参构造。
2、holder.get()也就是ThreadLocal的get方法。
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 根据当前线程获取 静态内部类 map变量
ThreadLocalMap map = getMap(t);
if (map != null) {// 存在map就获取相应的value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果不存在则获取初始化的值
return setInitialValue();
}
按上面代码的流程依次进入相应的代码块进行阅读理解
ThreadLocalMap getMap(Thread t) {
// 返回的是线程内部的私有属性
return t.threadLocals;
}
。。。
// Thread里面的私有属性threadLocals 按上面的流程走下来这里是为null,直接进入setInitialValue()
ThreadLocal.ThreadLocalMap threadLocals = null;
private T setInitialValue() {
// 初始化方法,这个方法可以在new ThreadLoacal的时候进行复写设置threadlocal应该存储的值,如果没有复写就返回null,由于之前new的时候没有复写,这里就是null
T value = initialValue();
Thread t = Thread.currentThread();
// 这里还是null
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 进入创建localMap的代码块
createMap(t, value);
return value;
}
void createMap(Thread t, T firstValue) {
// 直接创建静态内部类,看看这是个什么map
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
。。。
// localmap的构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 创建一个localmap的内部数组Entry[],默认容量16,Entry是localmap的静态内部类
table = new Entry[INITIAL_CAPACITY];
// 这里计算数组的索引i,用实例对象holder的hashcode和容量-1即15做与运算。这个holder的hashcode是在实例化时设置的一个固定的值。这个值是0x61c88647的整数倍后取低位的32位。
// 也就是系统中每创建一个threadlocal,它的hashcode就在前一个实例的hashcode上加0x61c88647,按网上的说法是这个数处在int 整数的黄金分割位上,能减少计算出来的索引值出现冲突的情况。
// 假设这个holder就是系统中的第一个threadlocal,它的hashcode就是0x61c88647,那么计算出来的i就是7
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 给table[7] 赋值
// Entry 继承至 WeakReference 继承至 Reference,其内部就是一个Object的value属性,这里构造就将这个firstvalue(null)赋值给value,然后这个firstkey(holder)赋值给Reference的referent属性
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 这里设置的 table数组的阈值及16*0.75
setThreshold(INITIAL_CAPACITY);
}
...
// Entry的定义及构造
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
这是hashcode和下标的对照图,参照(https://www.jianshu.com/p/3c5d7f09dfbd)
3、走完上面的流程后来到get()拿到的就是初始化的value(null),按照构建lc的流程,此时就应该获取new lc并将lc set进holder,接下来就是threadlocal的set方法。
public void set(T value) {
Thread t = Thread.currentThread();
// 此时已经有了map,只是value是null
ThreadLocalMap map = getMap(t);
if (map != null)
// 开始设置真正的lc
map.set(this, value);
else
createMap(t, value);
}
private void set(ThreadLocal<?> key, Object value) {
// 拿到之前的创建的table数组
Entry[] tab = table;
// 默认16
int len = tab.length;
// 固定的hashcode 固定的i=7
int i = key.threadLocalHashCode & (len-1);
//
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 拿到tab[7]的e,这个e.get()就是去拿这个Entry里面的Reference的referent属性,那这个referent就是这个holder
ThreadLocal<?> k = e.get();
// 同一个对象肯定相等,然后赋值新的lc并返回
if (k == key) {
e.value = value;
return;
}
// 可能是出现hash冲突后?并且之前的被preHolder被回收出现内存泄漏
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 上面的循环是e不为空时进入,当有一个新的holder2直接set的时候,那么它的hashcode就是0xc3910c8e,i就是14,tab[14]为null,直接进入下面的流程
// 同之前一样构造entry并赋值tab[14]
tab[i] = new Entry(key, value);
int sz = ++size;
// 当没有删除无用的数组元素且元素个数超过阈值的时候就要扩容了。删除null元素和扩容的算法有点复杂牛皮
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 扩容到到原来的两倍
// 将老数据的元素搬移到新数组中,用新数组的容量和holder的hashcode计算下标位置,当出现hash冲突后则向后放直到成功
rehash();
}
4、接下来设置好lc后,就能从holder中取出lc了,那么这里就是threadlocal的get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 有map
if (map != null) {
// 拿到holder的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 拿到entry的value并返回
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
二、理解总结
1、每个线程维护自己的localmap,每个localmap是一个entry数组,数组的每个元素关联一个独立的holder(threadlocal),这个holder的hashcode决定了它在这个数组中的位置。
2、这个设计有点逆向思维的味道,很巧妙。根据个人理解,其具象的设计如下图。
3、惊魂bug原因分析:
-》bug版本的大致设计:
1、如果是a类用户,在第一个拦截器中holder.set(loginContext) 。
2、在第二个拦截器中判断holder.get()==null?解析b类用户并holder.set(loginContext):不处理放行
-》bug版本的现象及原因:
现象:a类用户登录后并进行一些请求,b类用户登录后并进行一些请求。刚开始两类用户的前几个操作都问题。当操作多次过多后,b类用户的操作便大概率出现无权限的校验错误。
原因:因为tomcat容器采用线程池来处理请求。当a类用户进入系统进行多次操作后,在多个线程的localmap中就存入了a类用户的loginContext.假设其中就有t1.而当b类用户再操作的时候,可能是处理过a类用户请求的t1又来处理b类用户的请求,那么这时t1里面就只有a类用户的loginContext,那么b的操作肯定就会出现权限错误的提示。
-》bug处理:
在每次经过拦截器的时候都进行一次holder.set(loginContext)操作来刷新该线程上一次处理过的请求的用户信息。
4、ThreadLocal的内存泄漏
看了(https://blog.csdn.net/puppylpg/article/details/80433271)和(https://zhuanlan.zhihu.com/p/58636499)的文章。个人理解得ThreadLocal的内存泄漏是不是这样的:
ThreadLocal.LocalMap.Entry的定义如下,其继承了弱引用。那么,每个实例local则被弱引用一次。当线程一直存活且实例local的强引用消失后,实例local将会被gc回收,那么entry将会在线程下一次使用ThreadLocal的时候被回收。如果Entry不采用弱引用实现,那么即便是实例local失去强引用,只要线程一直存活,那么实例local和entry都不会被回收,因为线程一直持有该local的强引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 线程池中的线程1 执行了一次下面的代码
ThreadLocal<Integer> holder=new ThreadLocal<>();
holder.set(1);
// 清楚local的强引用,gc的时候localmap中的Entry中的key即holder将被回收
// 而Entry的value将一直存在
holder=null;
// 如果这段代码再次执行或者线程1在其他地方创建了另一个holder2,
// 这个holder2 执行了 set get remove 其中之一的话,
// 其holder泄漏的value将被清除
holder2.set()中的避免内存泄漏方法
// 这是localMap的set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
// 这里清除key==null的entry的value
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 这里也会启发式地去清除key为null的entry的value
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
holder2.get()中的避免内存泄漏方法
// 这是localMap的get方法
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果先有set 那么有泄漏的地方就被清除了,这里拿到e就返回了
if (e != null && e.get() == key)
return e;
else
// 如果没有set就get,这里就是避免内存泄漏的方法
return getEntryAfterMiss(key, i, e);
}
holder2.remove()
// 这是localMap的remove方法
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
// 这里
expungeStaleEntry(i);
return;
}
}
}