Theadlocal原理详解

1、Theadlocal是什么

  Theadlocal是用于线程独享数据的,每个线程单独一份存储空间,每个ThreadLocal只能保存一个变量副本;相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值,很好的实现了线程封闭;

2、Threadlocal原理概要

  Threadlocal实现线程间单独保存一份变量是通过在Threadlocal中定义一个静态内部类ThreadlocalMap:

  该内部类ThreadLocalMap中使用Entry[]数组来存放线程的数据,而Entry是一个key-value的对象,key是ThreadLocal,因此只有当前线程才能取到自己线程存放的值,从而实现线程间的数据隔离

3、Threadlocal代码分析

  ThreadLocal中主要有4个方法withInitial()、set()、get()、remove()接下来我就逐一分析这些方法

  withInitial()

  创建ThreadLocal对象主要有两个方法,方法1:直接new ThreadLocal(),方法2:调用ThreadLocal.withInitial(),这两个方法有什么区别呢?惟一的区别就是没有默认的初始值,此时若通过get()方法便会获取到null,即线程变量存放的value为null

  /**
     * 创建线程变量,并初始化线线程变量的值
     * @param supplier 用于初始化线程变量的值*/
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
  /**
     * An extension of ThreadLocal that obtains its initial value from
     * the specified {@code Supplier}.
     */
    static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        private final Supplier<? extends T> supplier;

        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }

  set()方法

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

  可以看到,这里是先获取当前的工作线程,然后拿到线程的 threadLocals 变量。这是一个 ThreadLocalMap 型的对象,是一个 Map 型的数据结构,实际的值就是保存在这里面。set() 方法中包含了1)获取;2)先创建再设置  

  get()方法

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();
    }

  这里的get方法同理,也是先获取线程的threadLocals 变量,再获取,若map为空则,调用setInitialValue()方法,先初始化一个值再返回,如果创建ThreadLocal使用的withInitial()方法,这里就会调用withInitial()方法中的Supplier来获取一个初始值,我们再来看看setInitialValue ()方法,这里就和set方法一样,不过setInitialValue()方法返回了初始值

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

  remove()方法

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

  remove()方法也很简单,获取当前线程的threadLocals,不为空便remove掉

  threadLocal中的方法都比较简单,实现线程变量的设置、获取、移除的具体操作都写在了ThreadLocalMap中,下面我们来看看ThreadLocalMap中具体是如何实现的

  ThreadlocalMap

  从上面的 get() 与 set() 方法可以看到,实际上 ThreadLocal 的值是保存在 ThreadLocalMap 这样一个结构中。ThreadLocalMap 是一个非常类似于 HashMap 的结构。它以 ThreadLocal 作为 key,value 就是 ThreadLocal 的值。每个线程都有一个 ThreadLocalMap 对象,而 ThreadLocalMap 中保存着当前线程拥有的所有 ThreadLocal。下面简单的看一下 ThreadLocaMap 的结构:

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;
            }
        }
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table; // 实际存放线程变量的位置
        private int size = 0;
        private int threshold; // Default to 0
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }
        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 的创建过程,非常像一个 HashMap。其中的关键几点:

  • 值以 Entry 的形式保存在一个数组中,ThreadLocal 作为 key(是一个弱引用WeakReference),value 就是对应的值;
  • 默认保持数量为 16,默认的 threshold 会计算为长度的 2/3;
  • 每个 ThreadLocal 都有自己的 threadLocalHashCode,用来计算它在数组中的索引;
  • nextIndex 与 prevIndex 用来安全的查找上一个或者下一个索引位置。

  通过上面的内容,我们基本已经知道 ThreadLocal 的大致工作原理。我们知道每个线程都有一个 ThreadLocalMap 结构,其中就保存着当前线程所持有的所有 ThreadLocal。ThreadLocal 本身只是一个引用,没有直接保存值,值是保存在 ThreadLocalMap 中,ThreadLocal 作为 key,值作为 value。而在ThreadLocal中的set(),get()方法最终的实现操作都是在ThreadLocalMap中实现的,接下来我们就来看看,ThreadLocalMap中具体怎么实现set(),get()

  设置Entry

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            // 通过hash值计算在数组中的位置
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                // 插入逻辑1:插入位置为key等于当前ThreadLocal,直接设置值
                if (k == key) {
                    e.value = value;
                    return;
                }
                // 插入逻辑2:插入位置为key出现key为null,说明弱引用被回收了,需要排序移动现有的值,再插入
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
                // 插入逻辑3:插入位置不为null,且key值也不为null,继续遍历下一个
            }
            //插入逻辑4:tab[i]为null,直接设置新的值
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

  通过 threadLocalHashCode 计算索引位置;

  查找和设置的过程,又可以细分如下:

    如果索引位置上还没有元素(Entry),也就是 tab[i] 为 null,这时候会将新的元素放入;
    如果索引位置上有元素,且元素的 key(ThreadLocal)非空,那么就寻找下一个索引位置;
    如果索引位置上有元素,且元素的 key 为 null,那么就用新的 Entry 替换掉原来的 Entry;
    如果索引位置上有元素,且元素的 key 就是当前的 ThreadLocal,那么直接将 Entry 的值进行替换;

  如果上述操作结果是新增了一个 Entry,那么会进行清理操作,并在满足特定条件的前提下进行 rehash 操作。

  ps:在上边出现了两种为null的情况需要区分开,第一个是tab[i] == null;第二个是 e=tab[i]; e.get()拿到该元素的key,key为null;key为null代表该对象Entry的ThreadLocal为null,但是该对象Entry还在,tab[i]所在位置是有值的;tab[i]为null代表这个位置是没有值的

  getEntry()

private Entry getEntry(ThreadLocal<?> key) {
            // 通过hashcode的值计算在数组中的位置,这里和hashMap原理相同
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            // 通过hashcode计算的位置存在值,且key和当前的ThreadLocal相等,则为该元素
            // 若key不相同时会查询下一个数据元素的key是否相同,这里与hashMap实现不同,ThreadLocalMap通过往数组后查询
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            //从当前位置挨个向后查询,出现key相同则返回对应的value,直到遇到下个元素为null时,说明该线程变量的值不存在
            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

   getEntry()方法threadLocalHashCode 计算索引位置,在索引位置开始往后查询直到出现下一个tab[i]为null,在中间找到key等于ThreadLocal的value,即为当前线程的value

  删除Entry

  删除节点的时候就不能像HashMap那样直接移除就可以了,因为出现地址下标冲突时,需要线性往后寻址,删除单个元素后需要将后边的元素往前移动,即重新计算元素的位置

  • 假如新创建的一个ThreadLocal计算索引位置为2,但tab[2]已有值,且ThreadLocal不相同
  • 该新建的ThreadLocal就需要检查位置3是否为null,tab[3]为null,写入tab[3]
  • tab[2]移除了
  • 前面的那个 ThreadLocal 值有更新,计算其索引位置是 2,此时发现 table[2] 不存在元素,则创建新的 Entry,放在位置 2

  这样就出现一个ThreadLocal同时放在了tab[2]和tab[3],而且值还不相同,这样显然是不对的,所以移除元素时需要重新计算删除节点后边的值,现在我们再来看源码

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            // 计算ThreadLocal在tab中的位置
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                // 通过key找到ThreadLocal所在位置的下标
                if (e.get() == key) {
                    e.clear();//清除当前线程变量引用
                    expungeStaleEntry(i);//重新计算该变量后面的值
                    return;
                }
            }
        }
private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            // 删除当前线程变量
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
            Entry e;
            int i;
            // 获取移除位置后边的值,直到tab[i]为null
            for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) { //刷新时如出现key为null的一起移除掉
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //当staleSlot后面元素不为null时重新计算位置
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
                        //重新寻找h所在位置,顺的新计算的位置线性往后寻找,找到为null的位置将其放入
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

   Entry替换

  在上边提到的设置Entry时,有一种情况是:插入位置出现key为null的情况,该情况下调用replaceStaleEntry(key, value, i)方法,该方法主要便是重新计算当前位置往前、往后的位置,直到前后都出现tab[i]为null,为什么还要往前?刚才set的时候前面的已经计算过,前面的值都是正确的,这里是为了防止出现一些场景,比如:

  • 正常逻辑来讲,此时 i = 1 与 i = 2 的位置,都是有元素存在的,所以应该把当前 ThreadLocal,放在 3 的位置;
  • 假如此时发生了 GC,偏偏好 i = 2 的位置上的元素被 GC 了,那么其实此时,应该将其放在 2 的位置;
  • 假如我们已经把该 ThreadLocal 放在了 3 的位置,这个时候 2 的位置发生了 GC;那么下次该 ThreadLocal 进行 set 操作时,会在 i = 2 的地方调用 replaceStaleEntry() 方法,这样就导致该 ThreadLocal 同时存在于 2 和 3 的位置。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            //向前检查,检查是否有key为null的值,若有记录下最前面key为null的下标
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            //向后扫描,检查是否出现key相等的情况,直到tab[i]为null
            for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //往后查找key相等的值,如果出现相等的key则交换tab[staleSlot]的值,已保证数据一致性
                if (k == key) {
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
                    //相等时表示前面没有可以清理的,但是现在后边有清理的
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //清理slotToExpunge后边所有的数据,即找到第一个需要开始清理的位置,往后清理,清理完成后结束
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                //出现key为null的情况,且往前没有需要清理的,那么第一个需要清理的位置为当前key为null的位置
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 存入当前ThreadLocal的值
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // slotToExpunge != staleSlot说明有需要清理的数据,以slotToExpunge作为第一个需要开始清理的位置
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
  • 第一步,向前扫描,检查前面是否有 key == null 的元素,也就是 GC 留下的坑;这里用 slotToExpunge 表示第一个 key == null 的元素位置,如果没有,那么 slotToExpunge 就是当前位置的索引,也就是 staleSlot;
  • 第二步,向后扫描,检查是否有 key == 当前 ThreadLocal 的元素。如果有,就将该元素放入到 staleSlot 位置,同时根据 slotToExpunge 的值,更新需要清理的元素位置,从 slotToExpunge 位置开始进行清理与 rehash,然后返回;
  • 第三步,第二步扫描过程中最终遇到元素为 null,说明后面没有 key == 当前 ThreadLocal 的元素,停止扫描,更新 slotToExpunge 位置,然后从 slotToExpunge 位置开始,进行清理与 rehash 操作。

  rehash()

  rehash方法会刷新所有的数据,重新计算位置,通过expungeStaleEntry方法删除掉tab中key为null的数据

private void rehash() {
            expungeStaleEntries();
            if (size >= threshold - threshold / 4)
                resize();
        }
private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }

  resize()方法

  这里的扩容阀值也挺奇怪的,threshold为数组长度的2/3,而扩容的时候又是大于threshold的3/4

  resize() 方法相对比较简单,它会创建一个新的两倍大小的数组,然后对原数组的元素进行遍历,依次将元素映射到新的数组中,采用同样的哈希映射与寻址方式。如果期间发现 key 为 null 的元素,会将 value 设置为空,即删除 value 的引用,防止内存泄漏。操作完成后,用新的数组替换原来的数组,同时设置新的 size 与 threshold。

private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {//删除key为null的元素
                        e.value = null; // Help the GC
                    } else {
                        //重新计算hash所在位置,减少冲突
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

   4、ThreadLocal中存在的内存泄露问题

  因为当前线程与ThreadLocal是强引用的关系,而ThreadLocal与ThreadLocalMap又是弱引用关系,若线程使用完毕没有手动remove,ThreadLocal被回收ThreadLocalMap中key值被回收,但是value值却是个强引用不会被回收,这样便会导致内存泄露;

  但是ThreadLocalMap已经在针对该方式处理内存泄露的问题,将ThreadLocalMap的key设置为弱引用,当其他线程操作时,出现地址碰撞,则会检查是否存在key为null的元素,如果有则将其回收;或者触发rehash 时也会被回收,这也是这里使用弱引用的好出。

  5、ThreadLocal在框架中个的应用

  Spring框架中存在一个RequestContextHolder对象,该对象存储了request对象中的信息,并提供了静态方法RequestContextHolder.getRequestAttributes()可以直接获取request中的参数,该对象是通过ThreadLocal来实现线程隔离的

  Spring security框架中的基础组件SecurityContextHolder,这是一个工具类,只提供一些静态方法。这个工具类的目的是用来保存应用程序中当前使用人的安全上下文,比如用户权限,如何保证取到的权限不是其他人的?这里也是通过ThreadLocal来实现

posted @ 2021-09-17 10:38  筱小2  阅读(233)  评论(0编辑  收藏  举报