Loading...
墨滴

UNREMITTINGLY

2021/12/21  阅读:48  主题:雁栖湖

ThreadLocal要这样答

ThreadLocal的作用是在线程隔离的基础上为线程上下文提供存储,传递的功能。也就是在同个线程中,存储在ThreadLocal中的数据,在线程运行的任何阶段都可以获取,应用,清除。


1使用场景

  • Spring MVC的RequestContextHolder的实现就是使用了ThreadLocal;
  • 跨层传递参数,有些时候第一层获得的一个变量值可能在第三层、甚至更深层的方法中才会被使用。
  • 有时候我们需要从上层传递一个参数到下层的方法,但是下层的方法新增一个参数的话,会违背开闭原则,如果依赖此方法的上层比较多,那修改此方法必然是一个不可取的方案(代码中难免会遇到这种不合理的代码)因此我们可以通过ThreadLocal来传递这个参数。

ThreadLocal的设计真的非常巧妙,看似自己保存了每个线程的变量副本,其实每个线程的变量副本是保存在线程对象中,那么自然就线程隔离了。

2日常应用

public final class OperationInfoRecorder {

private static final ThreadLocal<OperationInfoDTO> THREAD_LOCAL = new ThreadLocal<>();

    private OperationInfoRecorder() {
    }
    
    public static OperationInfoDTO get() {
        return THREAD_LOCAL.get();
    }
    
    public static void set(OperationInfoDTO operationInfoDTO) {
        THREAD_LOCAL.set(operationInfoDTO);
    }
    
    public static void remove() {
        THREAD_LOCAL.remove();
    }
    
}

//使用
OperationInfoRecorder.set(operationInfoDTO)
OperationInfoRecorder.get()

使用中需要注意的点:

  • static确保全局只有一个保存OperationInfoDTO对象的ThreadLocal实例,并且可避免内存泄露;

  • final确保ThreadLocal的实例不可更改。防止被意外改变,导致放入的值和取出来的不一致。

3源码

Thread类相关源码

public class Thread implements Runnable {

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

}

ThreadLocal相关源码


public class ThreadLocal<T> {

   public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }


public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
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;
            }
        }
//内部其他代码省略
}

}
 

通过上面的源码,可以看到Thread、ThreadLocal、ThreadLocalMap的关系,Thread对象有两个ThreadLocalMap类型的成员变量,ThreadLocalMap又是ThreadLocal类中的内部静态类,ThreadLocal提供get,set,remove方法。

源码解析

ThreadLocalMap的数据结构

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

上面是ThreadLocalMap的构造方法,ThreadLocalMap其实就是table数组,数组元素是Entry类型,Entry由key和value组成,key是通过WeakReference包装弱引用,弱引用会在每次垃圾回收的时候被回收。

ThreadLocalMap-table-Entry(key,value),key就是一个指向当前ThreadLocal对象的引用,只不过此引用是弱引用。

解析set方法

  • getMap方法获取当前线程的ThreadLocalMap

  • ThreadLocalMap为null

    1.进行初始化ThreadLocalMap 初始化table 初始化第一个Entry 赋值

    2.把ThreadLocalMap赋值给Thread的成员变量

  • ThreadLocalMap不为null

    1.调用ThreadLocalMap对象的set方法,参数是当前ThreadLocal对象和要保存的值value。

    2.通过key计算下标.

    3.从当前计算的坐标开始向右遍历

    如果有值并且key相等,更新值,结束
    如果有值并且key不相等,继续遍历
    如果有值,值已经失效,进行失效处理
    如果无值,循环结束
    

    4.如果尚未返回,就把值插入当前坐标下

解析get方法

  • 获取线程的ThreadLocalMap对象
  • 如果为空,开始初始化
  • 如果不为空 计算下标 如果无值返回null 如果有值 如果key相等,返回 如果不等继续向后循环 如果无效,做失效处理,然后继续循环 循环结束,没有找到,返回null

解析remove方法


public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
     
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;
                }
            }
        }

1.计算下标 2.从当前下标开始遍历 找到key相等的,失效 从当前坐标做失效处理

失效处理走的是这个方法 replaceStaleEntry(key, value, i); i是上面遍历到的值

1.向左遍历找到坐标最小的失效值的坐标,保存 2.向右遍历延续上面的遍历 如果找到就替换,并且交换位置 如果最终没找到,就在key计算出的坐标处更新

3.失效处理 从最小的失效值坐标处,向右遍历,把失效值都清空

注意:此方法在清空失效元素后,会重新计算下标,挪动元素位置。

4ThreadLocal的问题

1.内存泄露

在ThreadLocalMap中使用WeakReference包装后的ThreadLocal对象作为key,也就是说这里对ThreadLocal对象为弱引用。当ThreadLocal对象在ThreadLocalMap引用之外,再无其他引用的时候能够被垃圾回收

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

垃圾回收后,Entry对象的key变为null,value还被引用者,既然key为null,那么value就不可能再被应用,但是因为value被Entry引用着,Entry被ThreadLocalMap引用着,ThreadLocalMap被Thread引用着,因此线程不结束,那么给回收的内存就会一直回收不了。

很容易出问题的情况就是我们在使用线程池的时候,当使用线程池时候,就意味着线程重用问题的存在,这时候使用threadLocal存放一些数据时候的话,如果没有显示的做清除处理,就有可能会出现内存泄露问题,甚至导致业务逻辑出现问题。所以在使用线程池的时候需要特别注意在代码运行完后显式的去清空设置的数据,如果用自定义的线程池同样也会用到这样的问题。此时需要在finally代码块显式清楚threadLocal中的数据。

当然对于内存泄露问题,ThreadLocalMap也是做了相关处理的, ThreadLocalMap在get和set的时候,会把遇到的key为null的entry清理掉。也就是上面源码中说的失效处理,不过这样做也不能100%保证能够清理干净。

我们可以通过以下两种方式来避免这个问题:

  • 把ThreadLocal对象声明为static,这样ThreadLocal成为了类变量,生命周期不是和对象绑定,而是和类绑定,延长了声明周期,避免了被回收;
  • 在使用完ThreadLocal变量后,手动remove掉,防止ThreadLocalMap中Entry一直保持对value的强引用。导致value不能被回收。

threadlocal的继承性

threadlocal不支持继承性:也就是说,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。

但是父线程设置上下文就无法被子线程获取吗?当然不是,thread类除了提供了threadLocals,还提供了inheritableThreadLocals,InheritableThreadLocal继承了ThreadLocal,这个类可是实现父线程的值在子线程中获取到。此类重写了ThreadLocal的三个方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    public InheritableThreadLocal() {
    }
    protected T childValue(T var1) {
        return var1;
    }
    ThreadLocalMap getMap(Thread var1) {
        return var1.inheritableThreadLocals;
    }
    void createMap(Thread var1, T var2) {
        var1.inheritableThreadLocals = new ThreadLocalMap(this, var2);
    }
}

此类是如何实现子线程获取父线程保存的值的呢? 下面代码是thread类的源码,在创建一个线程时,thread初始化的innt方法中会去判断父线程的 inheritThreadLocals中是否有值,如果有,直接赋值给子线程

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

InheritableThreadLocals的使用方式

private static final ThreadLocal<OperationInfoDTO> THREAD_LOCAL = new InheritableThreadLocals <OperationInfoDTO>();

其他问题

ThreadLocalMap采用开放寻址法插入数据。

为什么使用开地址法 在开放寻址法中,所有的数据都存储在一个数组中,比起链表法来说,冲突的代价更高。 所以,使用开放寻址法解决冲突的散列表,装载因子的上限不能太大。这也导致这种方法比链表法更浪费内存空间。 但是反过来看,链表法指针需要额外的空间,故当结点规模较小时,开放寻址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放寻址法中的冲突,从而提高平均查找速度。

使用中很少有大量ThreadLocal对象的场景。

UNREMITTINGLY

2021/12/21  阅读:48  主题:雁栖湖

作者介绍

UNREMITTINGLY