一个比喻
关于ThreadLocal有一个形象的比喻:健身房里的公共储存柜,这里的储存柜相当于threadLocalMap,柜子的钥匙好比threadLocal引用,去健身的人好比是一个个thread,每个健身的人都有一个分配给自己的钥匙,通过这个钥匙可以找到对应的储存柜然后存放自己的私人物品。当一个人使用完柜子后这个柜子可以分配给后来的人使用。

threadLocal,threadLocalMap,entry[],thread的关系

threadLocal类里有个内部类threadLocalMap,而entry[]是threadLoacalMap里的内部类,它继承了WeakReference,自身有个Object类型的value属性。thread类有个threadLocal.threadLocalMap属性。

static class ThreadLocalMap {

       
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
.....
}
  • 使用时需要使用static final修饰吗?
    先说fianl,使用final修饰其实是个‘普通’问题,这个与threadLocal并没有很直接的关系:对于基本类型不让再修改;对于引用类型不能再指向其它对象。使用static修饰的变量是类变量,不管创建多少对象始终都是一个对象,threaLocal是使用空间换时间的一种思想,使用static修饰后是为了减少对象的创建,减少空间占用。当然怎么使用还是需要结合业务场景。
  • set方法
        public void set(T value) {
            //获取当前线程
            Thread t = Thread.currentThread();
            //取出上面线程里的threadLocalMap
            ThreadLocalMap map = getMap(t);
            if (map != null)
                //如果threaLocal是static final修饰则这里的this会是同一个threadLocal
                map.set(this, value);
            else
                createMap(t, value);
        }
        
        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //获取Entry数组下标
            int i = key.threadLocalHashCode & (len-1);
            //循环里先取出对应下标的entry对象,接着判断是对象是否为空。
            //for里的语句3是在寻找下一个entry,也就是发生了碰撞后采用了开放定
            //址法法的线性探测(HashMap采用的是链表法)。
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //对应下标下有值,如果引用相等则进行覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
                //说明当前这个entry已经没有线程,或者被执行了remove方法,
                //再使用则直接使用并做一些清理工作
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //对应下标值为空,说明没有冲突,直接将值放入entry数组
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //未能进行清理且Entry的数量已经达到了列表的扩容阈值的2/3则进行扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
  • 其它
  1. 出现hash冲突是能过开放定址法里的线性探测解决。
  2. 在进行get查询时如果是出现了冲突的key,会继续向后遍历并比较key来获取对应的value。
  3. 扩容的过程中也会进行清理Entry的逻辑,并将新的entry数组长度扩大到旧数组的2倍,同时计算新的hash值后将旧的数据移到新的entry里。
  4. 在get,set,rehash,remove方法里如果条件满足也会进行清理工作。

内存泄漏问题
在线程池场景下线程使用完并不会销毁而是归还到池里,如果使用ThreadLocal会导致线程一直持有threadlocal,也就是thread->threadlocal->threadlocalmap->entry<key,value>这样一个引用链路,只有当remove方法被调用后,key会被置为null,而key是一个WeakReference,这样在GC的时候这个entry才会被回收。

说说InheritableThreadLocal
主线程派生出子线程后,在主线程里放入threadLocal的值在子线程里是取不到的,而放在InheritableThreadLocal里的值就可以在子线程里取出来,创建Thread对象时,会判断父线程中inheritableThreadLocals是否不为空,如果不为空,则会将父线程中inheritableThreadLocals中的数据复制到自己的inheritableThreadLocals中。这样就实现了父线程和子线程的上下文传递:

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ......
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        ......
    }

这样父子线程间传值的问题就解决了,但这种方式在线程池里行不通,可以如下操作:在创建Task的时候使用一个变量将业务线程里的inheritThreadLocal设置的值保存下来,然后在线程池里的线程执行过程中用上一步存下来的值赋给当前线程,示例可以参考:线程池InheritableThreadLocal问题,另外阿里的TransmittableThreadLocal已做了很好的封装,在使用时需要注意一点:
在使用TransmittableThreadLocal的同时,需要使用TtlExecutors对线程池进行封装,说明:ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析

最后说说FastThreadLocal
可以参考:原来这就是比 ThreadLocal 更快的玩意

  1. 不再使用hash算法,也不再使用map数据结构而是一个Object 数组,数组第一个位置存储着一个set<FastThreadLocal>对象,然后数组后面的位置都放 value,其中fastthreadlocal里存在指向value的指针,这样就可以进行对应,如下图。
  2. 为每个FastThreadLocal生成唯一一个index,避免ThreadLocal里的hash冲突。
  3. get,set方法里会注册后台任务使用后台线程进行清理工作,而ThreadLocal里是同步清理,这会对get,set性能有影响。(不过两者都建议手动进行清理)
  4. 扩容时不再需要rehash。
  5. FastThreadLocal使用字节填充解决伪共享。
    image.png

步履不停
38 声望13 粉丝

好走的都是下坡路