ThreadLocal基本源码分析


前言

系统中的登录上下文使用了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;
                }
            }
        }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值