深入理解ThreadLocal原理

893 阅读9分钟

「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战」。         

ThreadLocal到底是什么?

首先看看如何使用:

从上面结果看5个线程通过ThreadLocal拿到的变量互不影响。可以达到线程安全。

那么他是如何做到的呢?底层结构如何?

翻看ThreadLocal源码,并没有看到有存储数据的成员属性,那么数据存在哪里呢?

通过查看get方法源码,这个方法不用任何参数, 发现他是先获取当前线程对象,然后从当前线程对象获取一个ThreadLocalMap对象,然后从这个map对象把this(当前ThreadLocal对象)获取数据。 

public T get() {        //获取当前线程对象        Thread t = Thread.currentThread();        //从线程对象获取ThreadLocalMap        ThreadLocalMap map = getMap(t);        if (map != null) {            //当前ThreadLocal对象,从map中查找值            ThreadLocalMap.Entry e = map.getEntry(this);            if (e != null) {                @SuppressWarnings("unchecked")                T result = (T)e.value;                return result;            }        }        //ThreadLocalMap还未初始化,创建一下并赋予初始值        return setInitialValue();    }

初始化ThreadLocalMap 

private T setInitialValue() {        //获取初始化值        T value = initialValue();        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)            map.set(this, value);        else            //创建ThreadLocalMap并初始化值            createMap(t, value);        return value;    }

初始化线程t的ThreadLocalMap,并赋值 

void createMap(Thread t, T firstValue) {        t.threadLocals = new ThreadLocalMap(this, firstValue);}

接下来看下ThreadLocalMap在Thread中是怎么存放的。

public class Thread implements Runnable {        //线程成员变量,也就是每个线程都有个ThreadLocalMap    ThreadLocal.ThreadLocalMap threadLocals = null;    ///线程成员变量,这个后面再分析    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;}

从线程类源码看出,每个线程对象都会有一个ThreadLocalMap属性。

可以得出结论:ThreadLocal其实不存储数据,他是一个工具类,间接操作Thread对象中的ThreadLocalMap变量的。

既然数据都存在ThreadLocalMap,我们分析ThreadLocalMap的结构和底层实现。 

首先理一下ThreadLocalMap和线程,还有ThreadLocal三者的关系 

通过这个图可以看出Thread类中持有一个ThreadLocalMap引用,其实就是一个Entry类型的数组。Entry的key是ThreadLocal类型的,value 是Object 类型。也就是一个ThreadLocalMap可以持有多个ThreadLocal。 

看下类图 

捋一捋关系:

1、Thread持有ThreadLocalMap的引用,他们是1对1关系。

2、Entry是ThreadLocalMap的内部类,并且ThreadLocalMap持有Entry类型的数组。也就是一个ThreadLocalMap对应多个Entry。

3、ThreadLocal和ThreadLocalMap的关系是最难描述的,因为

ThreadLocalMap是ThreadLocal的子类,而ThreadLocalMap中存储的key类型是ThreadLocal,并且ThreadLocal是弱引用类型的。

看下他们的代码关系: 

public class ThreadLocal<T> {    //内部类    static class ThreadLocalMap {        /**         * 存储数据的条目,key是WeakReference弱引用类型的ThreadLocal,         * key直接用WeakReference管理。         * 如果get方法(key==null)锁门条目不存在了,会主动清除,         * 避免内存泄漏的(分配的内存,没用了,但是没被回收)         */static class Entry extends WeakReference<ThreadLocal<?>> {            /** The value associated with this ThreadLocal. */            Object value;            Entry(ThreadLocal<?> k, Object v) {                super(k);                value = v;            }        }

为什么ThreadLocalMap设计成内部类?

主要是说明ThreadLocalMap 是一个线程本地的值,它所有的方法都是private 的,也就意味着除了ThreadLocal 这个类,其他类是不能操作ThreadLocalMap 中的任何方法的,这样就可以对其他类是透明的。同时这个类的权限是包级别的,也就意味着只有同一个包下面的类才能引用ThreadLocalMap 这个类,这也是Thread 为什么可以引用ThreadLocalMap 的原因,因为他们在同一个包下面。

虽然Thread 可以引用ThreadLocalMap,但是不能调用任何ThreadLocalMap 中的方法。这也就是我们平时都是通过ThreadLocal 来获取值和设置值。 

这样设计的好处是什么? 

ThreadLdocalMap 对使用者来说是透明的,可以当作空气,我们一直使用的都是ThreadLocal,这样的设计在使用的时候就显得简单,然后封装性又特别好。 

set方法源码分析:

public void set(T value) {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)        //操作ThreadLocalMap,设置数据,key是ThreadLocal对象。            map.set(this, value);        else            createMap(t, value);    }        ThreadLocalMap getMap(Thread t) {        return t.threadLocals;    }​   //第一次,将线程ThreadLocalMap初始化好。    void createMap(Thread t, T firstValue) {        t.threadLocals = new ThreadLocalMap(this, firstValue);    }

下面注意看ThreadLocalMap的set方法 

private void set(ThreadLocal<?> key, Object value) {//我们不像get()那样使用快速路径,因为使用set()创建新条目//与替换现有条目至少一样普遍,在这种情况下,快速路径经常会失败。            Entry[] tab = table;            int len = tab.length;            //计算下标            //哈希魔数(增长数),也是带符号的32位整型值黄金分割值的取正            int i = key.threadLocalHashCode & (len-1);      //这里不断找下一个下标,直到找到数组下标位置为null的下标      //这里处理hash冲突,使用的是线性探测方法。            for (Entry e = tab[i];                 e != null;                 //线性探测方法 解决hash冲突                 e = tab[i = nextIndex(i, len)]) {                //key                ThreadLocal<?> k = e.get();                //ThreadLocal找到了 替换旧值                if (k == key) {                    e.value = value;                    return;                }                //key已经被回收了                if (k == null) {                //陈旧数据替换  替换成本次新set的key,value                    replaceStaleEntry(key, value, i);                    return;                }            }            //构建新节点,存到下标i位置            tab[i] = new Entry(key, value);            int sz = ++size;            //是否要扩容了            if (!cleanSomeSlots(i, sz) && sz >= threshold)                rehash();        }​/**  * Increment i modulo len.*/ private static int nextIndex(int i, int len) {            //每次下标加1,线性查找            return ((i + 1 < len) ? i + 1 : 0); }

看完上面的方法,就有疑问了,为什么ThreadLocalMap采用开放地址法来解决哈希冲突?

jdk 中大多数的Hash类都是采用了链地址法来解决hash冲突,为什么ThreadLocalMap 采用开放地址法来解决哈希冲突呢?首先我们来看看这两种不同的方式:

1、链地址法

这种方法的基本思想是将所有哈希地址为i的元素构成一个单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在这个链中进行。 

2、开放地址法

这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。 

链地址法和开放地址法的优缺点

开放地址法:

1、容易产生堆积问题,不适于大规模的数据存储。

2、散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。

3、删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。

链地址法:

1、处理冲突简单,且无堆积现象,平均查找长度短。

2、链表中的结点是动态申请的,适合构造表不能确定长度的情况。

3、删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

指针需要额外的空间,故当结点规模较小时,开放地址法较为节省空间。 

ThreadLocalMap采用开放地址法原因

1、ThreadLocal类中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table。

2、ThreadLocal 往往存放的数据量不会特别大(而且key是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低。 

自动清理源码:


private void replaceStaleEntry(ThreadLocal<?> key, Object value,                                       int staleSlot) {            Entry[] tab = table;            int len = tab.length;            Entry e;​            // Back up to check for prior stale entry in current run.            // We clean out whole runs at a time to avoid continual            // incremental rehashing due to garbage collector freeing            // up refs in bunches (i.e., whenever the collector runs).            int slotToExpunge = staleSlot;            for (int i = prevIndex(staleSlot, len);                 (e = tab[i]) != null;                 i = prevIndex(i, len))                 //找到最小的一个被回收的下标 默认是staleSlot                if (e.get() == null)                    slotToExpunge = i;            for (int i = nextIndex(staleSlot, len);                 (e = tab[i]) != null;                 i = nextIndex(i, len)) {                ThreadLocal<?> k = e.get();​                 //找到了相同的key                if (k == key) {                    e.value = value;                    //交换位置,这个时候staleSlot位置上的变成新的有用的数据 i位置无用                    //为什么要交换 不交换 的时候大坐标位置上存key 下次set会直接存入小下标位置 导致两个相同的key 出现数据错乱问题                    tab[i] = tab[staleSlot];                    tab[staleSlot] = e;​                    // Start expunge at preceding stale entry if it exists                    没有找到无效的key                    if (slotToExpunge == staleSlot)                    //slotToExpunge 设置成无效                        slotToExpunge = i;                   // 回收slotToExpunge                  cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);                    return;                }​                // If we didn't find stale entry on backward scan, the                // first stale entry seen while scanning for key is the                // first still present in the run.                // 更新slotToExpunge为最大需要回收的key                 if (k == null && slotToExpunge == staleSlot)                    slotToExpunge = i;            }​            // If key not found, put new entry in stale slot            tab[staleSlot].value = null;            tab[staleSlot] = new Entry(key, value);​            // If there are any other stale entries in run, expunge them            if (slotToExpunge != staleSlot)                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);        }                private int expungeStaleEntry(int staleSlot) {            Entry[] tab = table;            int len = tab.length;​            tab[staleSlot].value = null;            tab[staleSlot] = null;            size--;            Entry e;            int i;            for (i = nextIndex(staleSlot, len);                 (e = tab[i]) != null;                 i = nextIndex(i, len)) {                ThreadLocal<?> k = e.get();                if (k == null) {                //这里设置为null ,方便让GC 回收                    e.value = null;                    tab[i] = null;                    size--;                } else {                //这里主要的作用是由于采用了开放地址法,所以删除的元素是多个冲突元素中的一个,需要对后面的元素作                //处理,可以简单理解就是让后面的元素往前面移动                //为什么要这样做呢?主要是开放地址寻找元素的时候,遇到null 就停止寻找了,你前面k==null                //的时候已经设置entry为null了,不移动的话,那么后面的元素就永远访问不了了,下面会画图进行解释说明                                    int h = k.threadLocalHashCode & (len - 1);                    //他们不相等,说明是经过hash 是有冲突的                    if (h != i) {                        tab[i] = null;​                        while (tab[h] != null)                            h = nextIndex(h, len);                        tab[h] = e;                    }                }            }            return i; }
//这个方法是从i 开始往后遍历(i++),寻找过期对象进行清除操作 private boolean cleanSomeSlots(int i, int n) {            boolean removed = false;            Entry[] tab = table;            int len = tab.length;            // 用do while 语法,保证 do 里面的代码至少被执行一次            do {                i = nextIndex(i, len);                Entry e = tab[i];                                if (e != null && e.get() == null) {                //如果遇到过期对象的时候,重新赋值n=len 也就是当前数组的长度                    n = len;                    removed = true;                    //在一次调用expungeStaleEntry 来进行垃圾回收(只是帮助垃圾回收)                    i = expungeStaleEntry(i);                }            } while ( (n >>>= 1) != 0);//无符号右移动一位,可以简单理解为除以2            return removed;        }

通过查看上面的源码,我们知道expungeStaleEntry() 方法是帮助垃圾回收的,我们还可以发现get和set方法都可能触发清理方法expungeStaleEntry(),所以正常情况下是不会有内存溢出的,但是如果我们没有调用get 和set 的时候就会可能面临着内存溢出,养成好习惯不再使用的时候调用remove(),加快垃圾回收,避免内存溢出,退一步说,就算我们没有调用get和set和remove方法,线程结束的时候,也就没有强引用再指向ThreadLocal中的ThreadLocalMap了,这样ThreadLocalMap 和里面的元素也会被回收掉,但是有一种危险是,如果线程是线程池的,在线程执行完代码的时候并没有结束,只是归还给线程池,这个时候ThreadLocalMap和里面的元素是不会回收掉的。

关于ThreadLocal的思考

ThreadLocal找到空key时候尝试清理一遍无效的entry,此时向前遍历是为了找到一个最小需要清理的entry下标。向后遍历是为了找到第一个相同key的下标,这里是为了解决key相同的时候,判断下标出错,有用的数据往前移动。

ThreadLocalMap的key设计为弱引用,可以起到标识key失效了,需要被回收,使用线性探测进行回收清理失效的数据。

ThreadLocal两种清除方式分开讨论

他们的应用场景不一样 remove方法,主动清除数据的机制。

而 set/get方法里的清理逻辑 是针对 ThreadLocal WeakReference.get=null 这个对象被回收了,value还存在的情况。

当一个ThreadLocal失去强引用,生命周期只能存活到下次gc前,此时ThreadLocalMap中就会出现key为null的Entry,当前线程无法结束,这些key为null的Entry的value就会一直存在一条强引用链,造成内存泄露。

解决方案:

建议将ThreadLocal变量定义成private static的,在调用ThreadLocal的get()、set()方法完成后,再调用remove()方法,手动删除不再需要的ThreadLocal。 

InheritableThreadLocal 理解

InheritableThreadLocal是ThreadLocal的子类,作用是用来共享父类的ThreadLocal数据。使用方法和ThreadLocal一样,通过模版方法设计模式,重写了getMap,createMap。


public class InheritableThreadLocal<Textends ThreadLocal<T> {        protected T childValue(T parentValue) {        return parentValue;    }​    //获取线程的inheritableThreadLocals    ThreadLocalMap getMap(Thread t) {       return t.inheritableThreadLocals;    }​    //设置线程的inheritableThreadLocals    void createMap(Thread t, T firstValue) {        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);    }}​​//创建线程的地方,会初始化inheritableThreadLocals变量if (inheritThreadLocals && parent.inheritableThreadLocals != null)            this.inheritableThreadLocals =                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

在Spring框架的web模块就用到了ThreadLocal和InheritableThreadLocal。用来对每个线程的请求Request属性进行存储。 

public abstract class RequestContextHolder  {​  private static final boolean jsfPresent =      ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());​  private static final ThreadLocal<RequestAttributes> requestAttributesHolder =      new NamedThreadLocal<>("Request attributes");​  private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =      new NamedInheritableThreadLocal<>("Request context");}​

ThreadLocal在框架中使用的比较多,工作中也有可能用的到,实现线程间数据独占使用,保证线程安全,了解一些原理对工作中也有帮助。